diff --git a/tests/e2e/campaignColumnPicker.e2e.spec.ts b/tests/e2e/campaignColumnPicker.e2e.spec.ts index cc483974..a4ddabe6 100644 --- a/tests/e2e/campaignColumnPicker.e2e.spec.ts +++ b/tests/e2e/campaignColumnPicker.e2e.spec.ts @@ -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 () => diff --git a/tests/e2e/campaignMunicipalities.e2e.spec.ts b/tests/e2e/campaignMunicipalities.e2e.spec.ts index 037a49ca..a3ebe785 100644 --- a/tests/e2e/campaignMunicipalities.e2e.spec.ts +++ b/tests/e2e/campaignMunicipalities.e2e.spec.ts @@ -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(), diff --git a/tests/e2e/campaignMunicipalityResponsiveColumns.e2e.spec.ts b/tests/e2e/campaignMunicipalityResponsiveColumns.e2e.spec.ts index e49cbc3e..1658c9ac 100644 --- a/tests/e2e/campaignMunicipalityResponsiveColumns.e2e.spec.ts +++ b/tests/e2e/campaignMunicipalityResponsiveColumns.e2e.spec.ts @@ -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) })