test: stabilize responsive municipality journeys

This commit is contained in:
Francisco Solla 2026-08-05 15:00:33 -03:00
parent 2f8d16881d
commit 8abf18ecf9
2 changed files with 72 additions and 28 deletions

View file

@ -26,6 +26,30 @@ const showAllMunicipalityColumns = (page: Page, url: string) =>
},
])
const municipalityContainer = (page: Page) =>
page.locator('[data-container="municipality-list"]').last()
const visibleMunicipalityButton = (page: Page, name: string) =>
municipalityContainer(page).getByRole('button', { name }).filter({ visible: true })
const ensureWideMunicipalityList = async (page: Page) => {
// The resizable panel hydrates independently from the RSC list and can
// change the container stage after a cell first becomes visible. These
// desktop cell journeys wait for the widest table stage; B158 exercises the
// rail transition itself in its dedicated spec.
await page.waitForLoadState('networkidle')
const container = municipalityContainer(page)
const width = await container.evaluate((element) => element.clientWidth)
const close = page.getByRole('button', { name: 'Fechar', exact: true }).filter({ visible: true })
// A collapsed Panel can leave its overflowing close button technically
// visible. Only click when the list itself proves the chat still consumes
// space; otherwise the click is a no-op against an already collapsed rail.
if (width < 78 * 16 && (await close.isVisible())) await close.click()
await expect
.poll(() => container.evaluate((element) => element.clientWidth))
.toBeGreaterThanOrEqual(78 * 16)
}
/**
* Core municipality-model journeys per role: coordinator strategy editing, advisor
* scoping, staff declare/estimate privacy boundary, leader lockdown, and staff-only
@ -121,12 +145,18 @@ test.describe('Municípios — jornadas por papel', () => {
`${campaign.baseURL}/campanha/municipios?q=${encodeURIComponent(municipality.name)}`,
)
await expect(campaignPageChrome(page, 'Municípios')).toBeVisible()
await ensureWideMunicipalityList(page)
// Name includes the município — a substring search can return neighbors
// (strict-mode flake when two "Editar assessores …" buttons match).
const advisorsTrigger = page.getByRole('button', {
name: `Editar assessores em ${municipality.name}`,
})
// A concurrent RSC navigation may briefly retain the previous segment.
// Scope the editor to the appended, active municipality container so a
// hidden copy cannot absorb Playwright's click retries until timeout.
const advisorsTrigger = visibleMunicipalityButton(
page,
`Editar assessores em ${municipality.name}`,
)
await expect(advisorsTrigger).toBeVisible()
await advisorsTrigger.click()
const advisorsPopover = page.locator('[data-slot="popover-content"]')
@ -160,11 +190,11 @@ test.describe('Municípios — jornadas por papel', () => {
// state, now holds the assignment.
await page.reload()
await expect(campaignPageChrome(page, 'Municípios')).toBeVisible()
await page
.getByRole('button', {
name: `Editar assessores em ${municipality.name} — ${advisor.name}`,
})
.click()
await ensureWideMunicipalityList(page)
await visibleMunicipalityButton(
page,
`Editar assessores em ${municipality.name} — ${advisor.name}`,
).click()
await expect(
page.locator('[data-slot="popover-content"]').getByRole('button', {
name: `Remover ${advisor.name}`,
@ -192,10 +222,12 @@ test.describe('Municípios — jornadas por papel', () => {
`${campaign.baseURL}/campanha/municipios?q=${encodeURIComponent(municipality.name)}`,
)
await expect(campaignPageChrome(page, 'Municípios')).toBeVisible()
await ensureWideMunicipalityList(page)
const advisorsTrigger = page.getByRole('button', {
name: `Editar assessores em ${municipality.name}`,
})
const advisorsTrigger = visibleMunicipalityButton(
page,
`Editar assessores em ${municipality.name}`,
)
await advisorsTrigger.click()
const advisorsPopover = page.locator('[data-slot="popover-content"]')
await expect(advisorsPopover).toBeVisible()
@ -231,11 +263,11 @@ test.describe('Municípios — jornadas por papel', () => {
// and the assignment, not just the local optimistic state.
await page.reload()
await expect(campaignPageChrome(page, 'Municípios')).toBeVisible()
await page
.getByRole('button', {
name: `Editar assessores em ${municipality.name} — ${newAdvisorName}`,
})
.click()
await ensureWideMunicipalityList(page)
await visibleMunicipalityButton(
page,
`Editar assessores em ${municipality.name} — ${newAdvisorName}`,
).click()
await expect(
page.locator('[data-slot="popover-content"]').getByRole('button', {
name: `Remover ${newAdvisorName}`,
@ -264,10 +296,12 @@ test.describe('Municípios — jornadas por papel', () => {
`${campaign.baseURL}/campanha/municipios?q=${encodeURIComponent(municipality.name)}`,
)
await expect(campaignPageChrome(page, 'Municípios')).toBeVisible()
await ensureWideMunicipalityList(page)
const deputiesTrigger = page.getByRole('button', {
name: `Editar dobradinhas em ${municipality.name}`,
})
const deputiesTrigger = visibleMunicipalityButton(
page,
`Editar dobradinhas em ${municipality.name}`,
)
await deputiesTrigger.click()
const deputiesPopover = page.locator('[data-slot="popover-content"]')
await expect(deputiesPopover).toBeVisible()
@ -305,7 +339,8 @@ test.describe('Municípios — jornadas por papel', () => {
// the assignment, not just the local optimistic state.
await page.reload()
await expect(campaignPageChrome(page, 'Municípios')).toBeVisible()
await page.getByRole('button', { name: `Editar dobradinhas em ${municipality.name}` }).click()
await ensureWideMunicipalityList(page)
await visibleMunicipalityButton(page, `Editar dobradinhas em ${municipality.name}`).click()
await expect(
page.locator('[data-slot="popover-content"]').getByRole('button', {
name: `Remover ${deputyName} (${party})`,
@ -374,14 +409,17 @@ test.describe('Municípios — jornadas por papel', () => {
`${campaign.baseURL}/campanha/municipios?q=${encodeURIComponent(municipality.name)}`,
)
await expect(campaignPageChrome(page, 'Municípios')).toBeVisible()
await ensureWideMunicipalityList(page)
// Anchored: a bare template regex on a municipality name collides with
// prefix-shared names (Conde/Condeúba — 23/435 measured in the catalog),
// and `exact` no longer matches because the accessible name appends the
// current signal state ("— Sem sinal").
const signalTrigger = page.getByRole('button', {
name: new RegExp(`^Registrar sinal em ${municipality.name} —`),
})
const signalTrigger = municipalityContainer(page)
.getByRole('button', {
name: new RegExp(`^Registrar sinal em ${municipality.name} —`),
})
.filter({ visible: true })
await expect(signalTrigger).toBeVisible()
await signalTrigger.click()
@ -391,9 +429,13 @@ test.describe('Municípios — jornadas por papel', () => {
.getByRole('textbox', { name: 'Texto' })
.fill('Liderança local reportou visita adversária na feira.')
await signalForm.getByLabel('Tipo do sinal').selectOption('visita_adversario')
await signalForm.getByRole('button', { name: 'Registrar sinal', exact: true }).click()
await Promise.all([
expectPostResponse(page, '/campanha/municipios'),
signalForm.getByRole('button', { name: 'Registrar sinal', exact: true }).click(),
])
await expect(page.getByText('Sinal registrado.', { exact: true })).toBeVisible()
// The toast is intentionally transient; the persisted freshness label is
// the stable success contract and must arrive after the server response.
await expect(
page.getByRole('button', {
name: `Registrar sinal em ${municipality.name} — hoje`,

View file

@ -147,9 +147,11 @@ test.describe('B158 — colunas responsivas por largura do conteúdo', () => {
}) => {
test.slow()
// Calibrated so the real sidebar/chat transitions cross the 66rem stage
// even when both campaign rails consume their full production widths.
await page.setViewportSize({ width: 1920, height: 900 })
// At 1600px the real desktop rails straddle the responsive stages: the
// collapsed sidebar adds a column and the open chat removes one. A wider
// viewport can leave all optional columns visible in both states, which
// would only prove that the container shrank, not that its query reacted.
await page.setViewportSize({ width: 1600, height: 900 })
await loginWithAllColumns(page, campaign)
const container = municipalityContainer(page)
const viewportBefore = page.viewportSize()