diff --git a/eslint.config.mjs b/eslint.config.mjs index d97eff4b..280d6b12 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -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([ diff --git a/tests/e2e/campaignMunicipalities.e2e.spec.ts b/tests/e2e/campaignMunicipalities.e2e.spec.ts index 4abba4ad..f19284db 100644 --- a/tests/e2e/campaignMunicipalities.e2e.spec.ts +++ b/tests/e2e/campaignMunicipalities.e2e.spec.ts @@ -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) diff --git a/tests/unit/eslintConventions.unit.spec.ts b/tests/unit/eslintConventions.unit.spec.ts index 8a0dd22b..fb779cd9 100644 --- a/tests/unit/eslintConventions.unit.spec.ts +++ b/tests/unit/eslintConventions.unit.spec.ts @@ -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 }`, + ) + }) })