mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
Some checks are pending
Deploy / preflight (push) Waiting to run
Deploy / verify (push) Blocked by required conditions
Deploy / deploy staging (teqo-staging) (push) Blocked by required conditions
Deploy / requeue when main advanced (push) Blocked by required conditions
Deploy / deploy production (teqo-1313) (push) Blocked by required conditions
274 lines
11 KiB
TypeScript
274 lines
11 KiB
TypeScript
import type { Locator, Page } from '@playwright/test'
|
|
|
|
import { expect, test, waitForRouterSettled } from './fixtures/campaignE2EFixtures.js'
|
|
|
|
const REM_IN_PIXELS = 16
|
|
const municipalityColumnsCookie = encodeURIComponent('municipios:__none__')
|
|
|
|
// 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) => {
|
|
const htmlElement = element as HTMLElement
|
|
htmlElement.style.width = `${nextWidth}px`
|
|
htmlElement.style.maxWidth = `${nextWidth}px`
|
|
}, width)
|
|
await expect.poll(() => container.evaluate((element) => element.clientWidth)).toBe(width)
|
|
}
|
|
|
|
const visibleTriggerText = (trigger: Locator) =>
|
|
trigger.evaluate((element) => (element as HTMLElement).innerText.trim())
|
|
|
|
const visibleHeaders = (container: Locator) =>
|
|
container
|
|
.locator('thead th')
|
|
.evaluateAll((headers) =>
|
|
headers
|
|
.filter((header) => getComputedStyle(header).display !== 'none')
|
|
.map((header) => (header.textContent ?? '').replaceAll(/\s+/g, ' ').trim()),
|
|
)
|
|
|
|
const expectNoHorizontalOverflow = async (container: Locator) => {
|
|
await expect
|
|
.poll(() => container.evaluate((element) => element.scrollWidth <= element.clientWidth))
|
|
.toBe(true)
|
|
}
|
|
|
|
type ResponsiveCampaignFixture = {
|
|
fixtures: {
|
|
createCampaignUser: (
|
|
role: 'advisor' | 'coordinator',
|
|
input: { name: string },
|
|
) => Promise<{ email?: string | null; password: string }>
|
|
value: (prefix: string) => string
|
|
}
|
|
login: (page: Page, identifier: string, password: string) => Promise<void>
|
|
}
|
|
|
|
const loginWithAllColumns = async (
|
|
page: Page,
|
|
campaign: ResponsiveCampaignFixture,
|
|
role: 'advisor' | 'coordinator' = 'coordinator',
|
|
) => {
|
|
const user = await campaign.fixtures.createCampaignUser(role, {
|
|
name: campaign.fixtures.value(`Responsivo ${role}`),
|
|
})
|
|
await campaign.login(page, user.email!, user.password)
|
|
await page.context().addCookies([
|
|
{
|
|
name: 'campaign_columns',
|
|
value: municipalityColumnsCookie,
|
|
url: new URL(page.url()).origin,
|
|
},
|
|
])
|
|
await page.goto('/campanha/municipios')
|
|
await page.waitForLoadState('networkidle')
|
|
await expect(municipalityContainer(page)).toBeVisible()
|
|
}
|
|
|
|
const expectedHeadersAt = (width: number) => {
|
|
if (width < 48 * REM_IN_PIXELS) return []
|
|
|
|
const headers = ['Município', '2022', '2026', 'Nível', 'Classe']
|
|
if (width >= 54 * REM_IN_PIXELS) headers.push('Assessores')
|
|
// B172 — below 60rem the trend column disappears and its compact trigger
|
|
// moves into Ações; above it, the column shows the full badge and Ações has
|
|
// nothing to host (it is width-gated closed), so the two swap at 60rem.
|
|
if (width >= 60 * REM_IN_PIXELS) headers.push('Tendência')
|
|
if (width >= 66 * REM_IN_PIXELS) headers.push('Liderança')
|
|
if (width >= 72 * REM_IN_PIXELS) headers.push('Dobradinha')
|
|
if (width >= 78 * REM_IN_PIXELS) headers.push('Cobertura')
|
|
headers.push('Atualização')
|
|
if (width < 60 * REM_IN_PIXELS) headers.push('Ações')
|
|
return headers
|
|
}
|
|
|
|
test.describe('B158 — colunas responsivas por largura do conteúdo', () => {
|
|
test('crosses every container threshold without horizontal scrolling', async ({
|
|
campaign,
|
|
page,
|
|
}) => {
|
|
test.slow()
|
|
|
|
await loginWithAllColumns(page, campaign)
|
|
const container = municipalityContainer(page)
|
|
const cards = container.locator('[data-view="mobile-cards"]')
|
|
const table = container.getByRole('table')
|
|
const trendTrigger = table.getByRole('button', { name: /^Editar tendência política/ }).first()
|
|
const signalTrigger = table.getByRole('button', { name: /^Registrar atualização em/ }).first()
|
|
// Test around (and on) the three binary switches: cards↔table (48rem),
|
|
// trend text (60rem), signal text (84rem). Container queries use min-width.
|
|
const widths = [
|
|
48 * REM_IN_PIXELS - 1,
|
|
48 * REM_IN_PIXELS,
|
|
48 * REM_IN_PIXELS + 1,
|
|
54 * REM_IN_PIXELS - 1,
|
|
54 * REM_IN_PIXELS + 1,
|
|
60 * REM_IN_PIXELS - 1,
|
|
60 * REM_IN_PIXELS,
|
|
60 * REM_IN_PIXELS + 1,
|
|
66 * REM_IN_PIXELS - 1,
|
|
66 * REM_IN_PIXELS + 1,
|
|
72 * REM_IN_PIXELS - 1,
|
|
72 * REM_IN_PIXELS + 1,
|
|
78 * REM_IN_PIXELS - 1,
|
|
78 * REM_IN_PIXELS + 1,
|
|
84 * REM_IN_PIXELS - 1,
|
|
84 * REM_IN_PIXELS,
|
|
84 * REM_IN_PIXELS + 1,
|
|
]
|
|
|
|
for (const width of widths) {
|
|
await test.step(`${width}px`, async () => {
|
|
await setContainerWidth(container, width)
|
|
|
|
if (width < 48 * REM_IN_PIXELS) {
|
|
await expect(cards).toBeVisible()
|
|
await expect(table).toBeHidden()
|
|
} else {
|
|
await expect(cards).toBeHidden()
|
|
await expect(table).toBeVisible()
|
|
expect(await visibleHeaders(container)).toEqual(expectedHeadersAt(width))
|
|
expect(await visibleTriggerText(trendTrigger)).toBe(
|
|
width >= 60 * REM_IN_PIXELS ? 'Não registrada' : '',
|
|
)
|
|
// B172 — below 60rem the column is gone and the compact trigger lives
|
|
// in Ações instead; `getByRole` skips display:none, so exactly one
|
|
// visible trend editor exists per row at every width.
|
|
await expect(
|
|
table
|
|
.locator('tbody tr')
|
|
.first()
|
|
.getByRole('button', { name: /^Editar tendência política/ }),
|
|
).toHaveCount(1)
|
|
expect(await visibleTriggerText(signalTrigger)).toBe(
|
|
width >= 84 * REM_IN_PIXELS ? 'Sem sinal' : '',
|
|
)
|
|
}
|
|
|
|
await expectNoHorizontalOverflow(container)
|
|
})
|
|
}
|
|
})
|
|
|
|
test('sidebar and Sollinha change the column stage without changing the viewport', async ({
|
|
campaign,
|
|
page,
|
|
}) => {
|
|
test.slow()
|
|
|
|
// 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)
|
|
// B203: fresh sessions start closed — open the chat explicitly so the
|
|
// stage below starts from the assumed "sidebar open + chat open" state.
|
|
// OPS42 — dev-only settle before the click (see `waitForRouterSettled`).
|
|
await waitForRouterSettled(page)
|
|
await page
|
|
.getByRole('button', { name: 'Sollinha — Assistente virtual' })
|
|
.filter({ visible: true })
|
|
.click()
|
|
await expect(
|
|
page.getByRole('button', { name: 'Fechar', exact: true }).filter({ visible: true }),
|
|
).toBeVisible({ timeout: 20_000 })
|
|
const container = municipalityContainer(page)
|
|
const viewportBefore = page.viewportSize()
|
|
const widthWithSidebar = await container.evaluate((element) => element.clientWidth)
|
|
const headersWithSidebar = await visibleHeaders(container)
|
|
|
|
await page.getByRole('button', { name: 'Abrir ou fechar menu da campanha' }).click()
|
|
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)
|
|
|
|
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', exact: true })
|
|
.filter({ visible: true })
|
|
.click()
|
|
await expect
|
|
.poll(() => container.evaluate((element) => element.clientWidth))
|
|
.toBeGreaterThan(widthWithChat)
|
|
await expect
|
|
.poll(async () => (await visibleHeaders(container)).length)
|
|
.toBeGreaterThan(headersWithChat.length)
|
|
expect(page.viewportSize()).toEqual(viewportBefore)
|
|
await expectNoHorizontalOverflow(container)
|
|
})
|
|
|
|
test('releases the Dobradinhas column to the advisor at full width (B176)', async ({
|
|
campaign,
|
|
page,
|
|
}) => {
|
|
await loginWithAllColumns(page, campaign, 'advisor')
|
|
const container = municipalityContainer(page)
|
|
await setContainerWidth(container, 84 * REM_IN_PIXELS + 1)
|
|
|
|
await expect(container.getByRole('columnheader', { name: /Assessores/ })).toBeVisible()
|
|
// B176 (2026-08-09): the Dobradinhas column is staff-wide — the advisor
|
|
// sees it once the container has room, same as the coordinator/candidate.
|
|
await expect(container.getByRole('columnheader', { name: 'Dobradinha' })).toBeVisible()
|
|
await expectNoHorizontalOverflow(container)
|
|
})
|
|
|
|
test('keeps the pinned name and 44px edit targets on an iPad viewport', async ({
|
|
campaign,
|
|
page,
|
|
}) => {
|
|
await page.setViewportSize({ width: 834, height: 1194 })
|
|
await loginWithAllColumns(page, campaign)
|
|
const container = municipalityContainer(page)
|
|
// Force the container past 48rem on the iPad viewport — the sidebar
|
|
// normally leaves too little content width for the table to appear.
|
|
await setContainerWidth(container, 48 * REM_IN_PIXELS + 1)
|
|
const table = container.getByRole('table')
|
|
await expect(table).toBeVisible()
|
|
|
|
const stickyPosition = await table
|
|
.getByRole('columnheader', { name: /Município/ })
|
|
.evaluate((element) => getComputedStyle(element).position)
|
|
expect(stickyPosition).toBe('sticky')
|
|
|
|
for (const trigger of [
|
|
table.getByRole('button', { name: /^Editar tendência política/ }).first(),
|
|
table.getByRole('button', { name: /^Registrar atualização em/ }).first(),
|
|
]) {
|
|
const box = await trigger.boundingBox()
|
|
expect(box?.width).toBeGreaterThanOrEqual(44)
|
|
expect(box?.height).toBeGreaterThanOrEqual(44)
|
|
}
|
|
await expectNoHorizontalOverflow(container)
|
|
})
|
|
})
|