test: align municipality e2e journeys with responsive columns

This commit is contained in:
Francisco Solla 2026-08-05 14:24:47 -03:00
parent cc60b82245
commit 2f8d16881d
3 changed files with 78 additions and 24 deletions

View file

@ -39,8 +39,8 @@ test.describe('Seletor de colunas', () => {
await campaign.login(page, email, password)
await page.goto('/campanha/municipios')
const levelHeader = page.getByRole('columnheader', { name: /Nível/ })
await expect(levelHeader).toBeVisible()
const votesHeader = page.getByRole('columnheader', { name: /2022/ })
await expect(votesHeader).toBeVisible()
const picker = page.getByRole('button', { name: /Mostrar ou ocultar colunas/ })
await picker.click()
@ -57,15 +57,15 @@ test.describe('Seletor de colunas', () => {
.toContain('municipios:__none__')
await picker.click()
await page.getByRole('checkbox', { name: 'Nível' }).click()
await page.getByRole('checkbox', { name: 'Votação 2022' }).click()
// Closing flushes the batched cookie write and refreshes the RSC.
await page.keyboard.press('Escape')
await expect(levelHeader).toHaveCount(0, REFRESH)
await expect(votesHeader).toHaveCount(0, REFRESH)
await expect(page.getByRole('button', { name: /1 oculta/ })).toBeVisible()
await page.reload()
await expect(page.getByRole('columnheader', { name: /Nível/ })).toHaveCount(0, REFRESH)
await expect(page.getByRole('columnheader', { name: /2022/ })).toHaveCount(0, REFRESH)
// The cookie carries one key per list: another list is untouched. The
// territories list is the honest neighbour here — its 27 rows come from
@ -79,9 +79,9 @@ test.describe('Seletor de colunas', () => {
await page.getByRole('button', { name: 'Restaurar todas' }).click()
await page.keyboard.press('Escape')
await expect(page.getByRole('columnheader', { name: /Nível/ })).toBeVisible(REFRESH)
await expect(page.getByRole('columnheader', { name: /2022/ })).toBeVisible(REFRESH)
await page.reload()
await expect(page.getByRole('columnheader', { name: /Nível/ })).toBeVisible(REFRESH)
await expect(page.getByRole('columnheader', { name: /2022/ })).toBeVisible(REFRESH)
await expect
.poll(
async () =>

View file

@ -1,3 +1,5 @@
import type { Page } from '@playwright/test'
import { SUPPORTER_REGISTRATION_CONSENT_KEY } from '../../src/lib/campaignConsentKeys.js'
import {
ensureLeasedConsent,
@ -15,6 +17,15 @@ import {
collectActionBoundingBoxes,
} from './helpers/actionGridGeometry.js'
const showAllMunicipalityColumns = (page: Page, url: string) =>
page.context().addCookies([
{
name: 'campaign_columns',
value: encodeURIComponent('municipios:__none__'),
url,
},
])
/**
* Core municipality-model journeys per role: coordinator strategy editing, advisor
* scoping, staff declare/estimate privacy boundary, leader lockdown, and staff-only
@ -26,6 +37,14 @@ import {
*/
test.describe('Municípios — jornadas por papel', () => {
test.beforeEach(async ({ page }) => {
// B158 keys the table stages to content width and gives fresh profiles a
// compact default. These pre-B158 journeys exercise specific desktop
// cells, so make that prerequisite explicit instead of finding hidden
// copies in the always-mounted mobile-card tree.
await page.setViewportSize({ width: 2400, height: 1000 })
})
test('coordinator opens the municipalities list, edits strategy and assigns an advisor', async ({
campaign,
page,
@ -41,6 +60,7 @@ test.describe('Municípios — jornadas por papel', () => {
const municipality = await fixtures.claimMunicipality()
await campaign.login(page, coordinator.email!, password)
await showAllMunicipalityColumns(page, campaign.baseURL)
await page.goto(`${campaign.baseURL}/campanha/municipios`)
await expect(campaignPageChrome(page, 'Municípios')).toBeVisible()
@ -96,6 +116,7 @@ test.describe('Municípios — jornadas por papel', () => {
const municipality = await fixtures.claimMunicipality()
await campaign.login(page, coordinator.email!, password)
await showAllMunicipalityColumns(page, campaign.baseURL)
await page.goto(
`${campaign.baseURL}/campanha/municipios?q=${encodeURIComponent(municipality.name)}`,
)
@ -117,16 +138,12 @@ test.describe('Municípios — jornadas por papel', () => {
// No "Salvar" button in this popover: selecting the option auto-saves the
// delta and renders it as a removable chip immediately.
const chip = advisorsPopover.getByRole('button', { name: `Remover ${advisor.name}` })
// Click the option itself: the inner text generic does not always trigger
// the combobox selection (measured — the popover stayed on "selected: você").
// Retry loop for the pre-hydration flake class (same as
// `checkRadixWhenHydrated`): a click before React attaches is a silent
// no-op, so the probe retries until the chip sticks.
await expect(async () => {
await search.fill(searchNamePart)
await option.click({ timeout: 1_000 })
await expect(chip).toBeVisible({ timeout: 4_000 })
}).toPass({ timeout: 20_000 })
// Opening this client-only popover already proves hydration. Wait for the
// write itself: the chip and trigger are optimistic and do not prove the
// assignment reached the database before the reload below.
await search.fill(searchNamePart)
await Promise.all([expectPostResponse(page, '/campanha/municipios/advisors'), option.click()])
await expect(chip).toBeVisible()
// The popover stays open after the write (auto-save, not submit+close).
await expect(advisorsPopover).toBeVisible()
@ -170,6 +187,7 @@ test.describe('Municípios — jornadas por papel', () => {
const newAdvisorName = fixtures.value('Novo Assessor')
await campaign.login(page, coordinator.email!, password)
await showAllMunicipalityColumns(page, campaign.baseURL)
await page.goto(
`${campaign.baseURL}/campanha/municipios?q=${encodeURIComponent(municipality.name)}`,
)
@ -241,6 +259,7 @@ test.describe('Municípios — jornadas por papel', () => {
const party = 'PCdoB'
await campaign.login(page, coordinator.email!, password)
await showAllMunicipalityColumns(page, campaign.baseURL)
await page.goto(
`${campaign.baseURL}/campanha/municipios?q=${encodeURIComponent(municipality.name)}`,
)
@ -307,6 +326,7 @@ test.describe('Municípios — jornadas por papel', () => {
const note = fixtures.value('Vereador confirmou apoio local')
await campaign.login(page, coordinator.email!, password)
await showAllMunicipalityColumns(page, campaign.baseURL)
await page.goto(
`${campaign.baseURL}/campanha/municipios?q=${encodeURIComponent(municipality.name)}`,
)
@ -349,6 +369,7 @@ test.describe('Municípios — jornadas por papel', () => {
const municipality = await fixtures.claimMunicipality()
await campaign.login(page, coordinator.email!, password)
await showAllMunicipalityColumns(page, campaign.baseURL)
await page.goto(
`${campaign.baseURL}/campanha/municipios?q=${encodeURIComponent(municipality.name)}`,
)
@ -433,6 +454,7 @@ test.describe('Municípios — jornadas por papel', () => {
// Advisor scope: only the administered municipality shows up.
await campaign.login(page, advisor.email!, password)
await showAllMunicipalityColumns(page, campaign.baseURL)
await page.goto(`${campaign.baseURL}/campanha/municipios`)
await expect(
page.getByRole('link', { name: administered.name, exact: true }).first(),

View file

@ -5,7 +5,10 @@ import { expect, test } from './fixtures/campaignE2EFixtures.js'
const REM_IN_PIXELS = 16
const municipalityColumnsCookie = encodeURIComponent('municipios:__none__')
const municipalityContainer = (page: Page) => page.locator('[data-container="municipality-list"]')
// During a concurrent RSC navigation Next may briefly retain the previous
// segment beside the incoming one. The current segment is appended last.
const municipalityContainer = (page: Page) =>
page.locator('[data-container="municipality-list"]').last()
const setContainerWidth = async (container: Locator, width: number) => {
await container.evaluate((element, nextWidth) => {
@ -62,8 +65,8 @@ const loginWithAllColumns = async (
},
])
await page.goto('/campanha/municipios')
await expect(municipalityContainer(page)).toBeVisible()
await page.waitForLoadState('networkidle')
await expect(municipalityContainer(page)).toBeVisible()
}
const expectedHeadersAt = (width: number) => {
@ -144,7 +147,9 @@ test.describe('B158 — colunas responsivas por largura do conteúdo', () => {
}) => {
test.slow()
await page.setViewportSize({ width: 1800, height: 900 })
// 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 })
await loginWithAllColumns(page, campaign)
const container = municipalityContainer(page)
const viewportBefore = page.viewportSize()
@ -155,17 +160,44 @@ test.describe('B158 — colunas responsivas por largura do conteúdo', () => {
await expect
.poll(() => container.evaluate((element) => element.clientWidth))
.toBeGreaterThan(widthWithSidebar)
await expect
.poll(async () => (await visibleHeaders(container)).length)
.toBeGreaterThan(headersWithSidebar.length)
const widthAfterSidebarToggle = await container.evaluate((element) => element.clientWidth)
const initiallyOpenChatClose = page
.getByRole('button', { name: 'Fechar', exact: true })
.filter({ visible: true })
if ((await initiallyOpenChatClose.count()) > 0) {
await initiallyOpenChatClose.click()
await expect
.poll(() => container.evaluate((element) => element.clientWidth))
.toBeGreaterThan(widthAfterSidebarToggle)
}
const widthWithoutSidebar = await container.evaluate((element) => element.clientWidth)
const headersWithoutSidebar = await visibleHeaders(container)
expect(headersWithoutSidebar.length).toBeGreaterThan(headersWithSidebar.length)
await page.getByRole('button', { name: 'Sollinha — Assistente virtual' }).first().click()
await page
.getByRole('button', { name: 'Sollinha — Assistente virtual' })
.filter({ visible: true })
.click()
await expect
.poll(() => container.evaluate((element) => element.clientWidth))
.toBeLessThan(widthWithoutSidebar)
await expect
.poll(async () => (await visibleHeaders(container)).length)
.toBeLessThan(headersWithoutSidebar.length)
const widthWithChat = await container.evaluate((element) => element.clientWidth)
const headersWithChat = await visibleHeaders(container)
await page.getByRole('button', { name: 'Fechar' }).last().click()
await page
.getByRole('button', { name: 'Fechar', exact: true })
.filter({ visible: true })
.click()
await expect
.poll(() => container.evaluate((element) => element.clientWidth))
.toBeGreaterThan(widthWithChat)
expect((await visibleHeaders(container)).length).toBeGreaterThan(headersWithChat.length)
await expect
.poll(async () => (await visibleHeaders(container)).length)
.toBeGreaterThan(headersWithChat.length)
expect(page.viewportSize()).toEqual(viewportBefore)
await expectNoHorizontalOverflow(container)
})