fix(e2e): B196 atividades deriva a data da fixture de hoje — desbloqueia deploy verify

A fixture B196 'chassis elsewhere' criava 5 atividades com startAt fixo em 2026-09-01. A aba default de /campanha/atividades (proximos) filtra status=confirmado e startAt >= now, então a partir de 02/09 a lista virou empty state, o scrollport perdeu overflow e a assert scrollHeight - clientHeight > 0 falhou nas 3 retries do verify do deploy de 2026-09-11 (run 34652323456), bloqueando o job de deploy.

Deriva o startAt de hoje (+7 dias, tempo civil da Bahia) como os specs de agenda já fazem, dá ao poll do scrollport o orçamento de 30s da classe de latência B196 documentada no arquivo, e adiciona guard ESLint (no-restricted-syntax escopado a tests/e2e) que bane ISO literal em propriedades start*/end*/date* de fixture — pinado em eslintConventions.unit.spec.ts.
This commit is contained in:
Francisco Solla 2026-09-12 00:58:30 -03:00
parent 8fefdb8cba
commit b4132b72c6
3 changed files with 62 additions and 4 deletions

View file

@ -43,6 +43,22 @@ const noHardcodedConsentId = {
'Never hardcode a Consent document id — resolve it by stable key via requireConsentByKey (keys in src/lib/campaignConsentKeys.ts).',
}
// E2E fixtures must derive dates from "today": a hardcoded ISO instant
// silently expires once it stops matching a time-windowed default view. The
// B196 activities fixture (`startAt: '2026-09-01...'`) emptied the default
// `proximos` tab and blocked the 2026-09-11 deploy verify. Derive with
// `civilDatePlusDays(formatBahiaCivilDate(new Date()), days)` +
// `parseBahiaDateTimeInput` (tests/e2e/helpers/agendaPeriodLabels.ts,
// src/lib/campaignTime.ts). Deliberately narrow (start*/end*/date* keys):
// state timestamps like `disabledAt`/`lastSyncedAt` are the scenario under
// test, not a time window.
const noHardcodedE2eFixtureDate = {
selector:
"Property[key.name=/^(start|end|date)/][value.type='Literal'][value.raw=/20[0-9]{2}-[0-9]{2}-[0-9]{2}T/]",
message:
'Never hardcode a fixture date in e2e specs — derive it from today (civilDatePlusDays + parseBahiaDateTimeInput); a literal expires and empties time-windowed lists (B196 deploy verify, 2026-09-11).',
}
const eslintConfig = [
...compat.extends('next/core-web-vitals', 'next/typescript'),
{
@ -212,6 +228,15 @@ const eslintConfig = [
'@typescript-eslint/no-unused-vars': ['warn', { args: 'none' }],
},
},
{
// E2E fixture dates must derive from "today" (B196 deploy verify,
// 2026-09-11): a hardcoded ISO instant expires and empties the
// time-windowed default view. Keeps the global as-never ban too.
files: ['tests/e2e/**/*.{ts,tsx}'],
rules: {
'no-restricted-syntax': ['error', noAsNeverCast, noHardcodedE2eFixtureDate],
},
},
prettier,
// Override default ignores of eslint-config-next.
globalIgnores([

View file

@ -1,6 +1,7 @@
import type { Page } from '@playwright/test'
import { SUPPORTER_REGISTRATION_CONSENT_KEY } from '../../src/lib/campaignConsentKeys.js'
import { formatBahiaCivilDate, parseBahiaDateTimeInput } from '../../src/lib/campaignTime.js'
import { fallbackDemandTitle } from '../../src/lib/demandTitle.js'
import { hookFilledCreateData } from '../../src/utilities/hookFilledData.js'
import {
@ -24,6 +25,7 @@ import {
assertThreeColumnActionGrid,
collectActionBoundingBoxes,
} from './helpers/actionGridGeometry.js'
import { civilDatePlusDays } from './helpers/agendaPeriodLabels.js'
const showAllMunicipalityColumns = (page: Page, url: string) =>
page.context().addCookies([
@ -1496,6 +1498,14 @@ test.describe('Municípios — barra colada e card denso (B196)', () => {
name: fixtures.value('Coordenadora B196 Ativ'),
})
const municipality = await fixtures.claimMunicipality()
// The default tab (`proximos`) only lists `confirmado` activities with
// `startAt >= now`, so the fixture start must be derived from "today" —
// a hardcoded date empties the list once it expires (deploy verify
// 2026-09-11: `scrollHeight - clientHeight` stuck at 0 on all retries).
const startAt = parseBahiaDateTimeInput(
`${civilDatePlusDays(formatBahiaCivilDate(new Date()), 7)}T13:00`,
)
if (!startAt) throw new Error('Falha ao montar horário da fixture de atividades.')
// Enough rows to make the scrollport scrollable once the top bar takes over.
for (let i = 0; i < 5; i++) {
await fixtures.payload.create({
@ -1503,7 +1513,7 @@ test.describe('Municípios — barra colada e card denso (B196)', () => {
data: hookFilledCreateData<'activity'>({
title: `Atividade B196 ${fixtures.value('x')}`,
status: 'confirmado',
startAt: '2026-09-01T13:00:00.000Z',
startAt,
municipality: municipality.id,
responsible: [{ relationTo: 'campaignUser', value: coordinator.id }],
}),
@ -1524,10 +1534,14 @@ test.describe('Municípios — barra colada e card denso (B196)', () => {
const headerBottomValue = await headerBottom(topBar)
expect((await form.boundingBox())!.y).toBeGreaterThan(headerBottomValue)
// The scrollport must actually scroll for the sticky to engage.
// The scrollport must actually scroll for the sticky to engage. Same B196
// stream-latency class as the sibling polls: the list chunk can land late
// under 4-worker load, so use the 30s budget instead of the 10s default.
const scrollport = contentScroll(page)
await expect
.poll(() => scrollport.evaluate((el) => el.scrollHeight - el.clientHeight))
.poll(() => scrollport.evaluate((el) => el.scrollHeight - el.clientHeight), {
timeout: 30_000,
})
.toBeGreaterThan(0)
await scrollport.evaluate((el) => el.scrollTo(0, el.scrollHeight))
await expect.poll(async () => (await form.boundingBox())!.y).toBe(headerBottomValue)

View file

@ -189,11 +189,30 @@ describe('ESLint conventions', () => {
it('allows async function and type-only exports from use server modules', async () => {
await expectValid(
'src/app/(campaign)/campanha/actions/futureAction.ts',
`'use server'\n\nexport type Result = { ok: true }\nexport const save = async () => ({ ok: true as const })\nexport async function load() { return 1 }\n`,
`'use server'\nexport type Result = { ok: true }\nexport const save = async () => ({ ok: true as const })\nexport async function load() { return 1 }\n`,
)
await expectValid(
'src/app/(campaign)/campanha/(app)/municipios/municipalityStaffEditMessages.ts',
`export const municipalityStaffEditSafeMessages = ['x'] as const\n`,
)
})
it('bans hardcoded ISO fixture dates in e2e specs but not state timestamps', async () => {
await expectInvalid(
'tests/e2e/futureFixture.e2e.spec.ts',
`const fixture = { startAt: '2026-09-01T13:00:00.000Z' }`,
'no-restricted-syntax',
)
await expectInvalid(
'tests/e2e/futureFixture.e2e.spec.ts',
`const fixture = { endDate: '2026-09-01T13:00:00.000Z' }`,
'no-restricted-syntax',
)
// `disabledAt`/`lastSyncedAt` are the state under test, not a time window;
// a computed `startAt` is exactly the sanctioned derivation.
await expectValid(
'tests/e2e/futureFixture.e2e.spec.ts',
`const fixture = { disabledAt: '2026-08-11T00:00:00.000Z' }\nconst startAt = 'dynamic'\nconst activity = { startAt }`,
)
})
})