teqo-backup/tests/e2e/admin.e2e.spec.ts
Francisco Solla 1dc4c55a8d fix(campaign): C140 — manual dispatch in 18 forms prevents React 19 form reset on error
Fix manual dispatch em 18 formulários de /campanha que usavam <form action={submitAction}>: campos não controlados voltavam ao defaultValue após qualquer action assentada (sucesso ou erro) do React 19.

- WizardStepFormChrome alterado uma vez (cobre 3 wizard steps)
- 15 forms recebem dispatch manual arquivo a arquivo
- AdvisorPasswordResetButton (2 micro-forms sem campo digitável) ficou como está
- Unit spec novo campaignWizardStepFormChrome.unit.spec.tsx
- CHANGELOG-AGENTS.md cleaned (98 duplicates removed)

Also fixes:
- campaignPageActor writable gate only blocks advisors (was blocking coordinators too)
- campaignLeaderships e2e stability (waitForLoadState before attribute assertion)
2026-08-23 17:42:59 -03:00

270 lines
9.8 KiB
TypeScript

import { login } from '../helpers/login'
import { cleanupTestUser, seedTestUser, testUser } from '../helpers/seedUser'
import { instagramStubUrlFor } from '../helpers/socialStub'
import { expect, test } from './fixtures/e2eTest'
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:3000'
test.describe('Admin Panel', () => {
test.beforeAll(async ({}, testInfo) => {
testInfo.setTimeout(90_000)
await seedTestUser()
})
test.beforeEach(async ({ page }) => {
await login({ page, serverURL: baseURL, user: testUser })
})
test.afterAll(async () => {
await cleanupTestUser()
})
test('can navigate to dashboard', async ({ page }) => {
await page.goto(`${baseURL}/admin`)
await expect(page).toHaveURL(`${baseURL}/admin`)
const dashboardArtifact = page.getByRole('heading', { name: 'Coleções', exact: true })
await expect(dashboardArtifact).toBeVisible()
})
test('can navigate to list view', async ({ page }) => {
await page.goto(`${baseURL}/admin/collections/users`)
await expect(page).toHaveURL(new RegExp(`^${baseURL}/admin/collections/users(?:\\?|$)`))
const listViewArtifact = page.getByRole('heading', { name: 'Usuários', exact: true })
await expect(listViewArtifact).toBeVisible()
})
test('can navigate to edit view', async ({ page }) => {
await page.goto(`${baseURL}/admin/collections/users/create`)
await expect(page).toHaveURL(/\/admin\/collections\/users\/[a-zA-Z0-9-_]+/)
const editViewArtifact = page.locator('input[name="email"]')
await expect(editViewArtifact).toBeVisible()
})
test('can exclude an Instagram post through the content board picker', async ({
page,
request,
}) => {
// Seed the social-feed global with an Instagram snapshot so the exclusion
// picker (S3) has posts to show. The snapshot is plain data — the picker
// lists it and writes `excludedItems` (the same mechanism YouTube uses).
const login = await request.post(`${baseURL}/api/users/login`, {
data: { email: testUser.email, password: testUser.password },
})
expect(login.ok()).toBeTruthy()
const { token } = await login.json()
const headers = { cookie: `payload-token=${token}` }
const snapshot = {
username: 'depjorgesolla',
posts: [
{
id: 'e2e-ig-muro-1',
caption: 'E2e Post do muro',
mediaType: 'IMAGE',
permalink: 'https://www.instagram.com/p/e2e-ig-muro-1/',
thumbnailUrl: null,
timestamp: '2026-08-18T10:00:00+00:00',
},
{
id: 'e2e-ig-reel-2',
caption: 'E2e Reel da caravana',
mediaType: 'REEL',
permalink: 'https://www.instagram.com/p/e2e-ig-reel-2/',
thumbnailUrl: null,
timestamp: '2026-08-18T09:00:00+00:00',
},
],
}
const seed = await request.post(`${baseURL}/api/globals/social-feed-settings`, {
headers,
data: {
instagramEnabled: true,
instagramAccessToken: 'e2e-ig-token',
instagramUserId: '17841400000000000',
instagramMaxItems: 3,
excludedItems: [],
instagramFeedSnapshot: snapshot,
},
})
expect(seed.ok()).toBeTruthy()
try {
await page.goto(`${baseURL}/admin/globals/social-feed-settings`)
await expect(page.getByText('E2e Post do muro')).toBeVisible()
const hideButton = page.getByRole('button', { name: 'Não exibir' }).first()
await expect(hideButton).toBeVisible()
await hideButton.click()
await expect(page.getByRole('button', { name: 'Reexibir' })).toBeVisible()
const saveResponse = page.waitForResponse(
(response) =>
response.url().includes('/api/globals/social-feed-settings') &&
response.request().method() === 'POST',
)
await page.getByRole('button', { name: 'Salvar' }).click()
await saveResponse
const read = await request.get(`${baseURL}/api/globals/social-feed-settings`, { headers })
expect(read.ok()).toBeTruthy()
const { excludedItems } = await read.json()
expect(excludedItems).toEqual([
expect.objectContaining({ platform: 'instagram', itemId: 'e2e-ig-muro-1' }),
])
} finally {
await request
.post(`${baseURL}/api/globals/social-feed-settings`, {
headers,
data: {
enabled: true,
youtubeEnabled: true,
youtubeApiKey: '',
youtubeChannelId: '',
youtubeMaxItems: 3,
instagramEnabled: true,
instagramAccessToken: '',
instagramUserId: '',
instagramMaxItems: 3,
excludedItems: [],
youtubeFeedSnapshot: null,
instagramFeedSnapshot: null,
instagramSyncStatus: null,
},
})
.catch(() => undefined)
}
})
test('shows the Instagram sync status and retries the sync from the panel', async ({
page,
request,
}) => {
// The seed POST fires the global's afterChange hook, whose sync hits the
// stub — put it in a known state first and let the hook persist the
// status so the panel has a real one to render.
const stubUrl = instagramStubUrlFor(baseURL)
const setStub = async (state: 'ok' | 'fail' | 'invalid-token') => {
const response = await request.post(`${stubUrl}/__stub/state`, { data: { state } })
expect(response.ok()).toBeTruthy()
}
await setStub('ok')
const login = await request.post(`${baseURL}/api/users/login`, {
data: { email: testUser.email, password: testUser.password },
})
expect(login.ok()).toBeTruthy()
const { token } = await login.json()
const headers = { cookie: `payload-token=${token}` }
const seed = await request.post(`${baseURL}/api/globals/social-feed-settings`, {
headers,
data: {
enabled: true,
instagramEnabled: true,
instagramAccessToken: 'e2e-ig-token',
instagramUserId: '17841400000000000',
instagramMaxItems: 3,
excludedItems: [],
instagramFeedSnapshot: null,
},
})
expect(seed.ok()).toBeTruthy()
const seedToken = async (accessToken: string) => {
const response = await request.post(`${baseURL}/api/globals/social-feed-settings`, {
headers,
data: {
enabled: true,
instagramEnabled: true,
instagramAccessToken: accessToken,
instagramUserId: '17841400000000000',
instagramMaxItems: 3,
excludedItems: [],
instagramFeedSnapshot: null,
},
})
expect(response.ok()).toBeTruthy()
}
try {
await page.goto(`${baseURL}/admin/globals/social-feed-settings`)
await expect(page.getByText(/Sincronizado · .* · 5 posts/)).toBeVisible()
// Credential change + save triggers the hook sync — with the API now
// rejecting the token (OAuthException), the save persists a failure
// status and the reload shows it with the retry button.
await setStub('invalid-token')
await seedToken('e2e-ig-token-2')
await page.goto(`${baseURL}/admin/globals/social-feed-settings`)
await expect(page.getByText('Falha na última sincronização')).toBeVisible()
await expect(page.getByText(/token do Instagram foi recusado/)).toBeVisible()
// Retry after fixing: the button runs the sync route → synchronized.
await setStub('ok')
await page.getByRole('button', { name: 'Tentar sincronizar de novo' }).click()
await expect(page.getByText(/Sincronizado · .* · 5 posts/)).toBeVisible()
const read = await request.get(`${baseURL}/api/globals/social-feed-settings`, { headers })
expect(read.ok()).toBeTruthy()
const { instagramSyncStatus } = await read.json()
expect(instagramSyncStatus).toEqual(
expect.objectContaining({
lastSyncAt: expect.any(String),
postCount: 5,
}),
)
expect(instagramSyncStatus.error).toBeUndefined()
// Fail-500 scene: a server-side API failure maps to the generic notice.
await setStub('fail')
await seedToken('e2e-ig-token-3')
await page.goto(`${baseURL}/admin/globals/social-feed-settings`)
await expect(page.getByText('Falha na última sincronização')).toBeVisible()
await expect(page.getByText(/indisponível/)).toBeVisible()
// Disabled scene: the master kill switch off renders the disabled box.
await setStub('ok')
await request.post(`${baseURL}/api/globals/social-feed-settings`, {
headers,
data: {
enabled: false,
instagramEnabled: true,
instagramAccessToken: 'e2e-ig-token',
instagramUserId: '17841400000000000',
instagramMaxItems: 3,
excludedItems: [],
instagramFeedSnapshot: null,
},
})
await page.goto(`${baseURL}/admin/globals/social-feed-settings`)
await expect(page.getByText('Instagram desativado')).toBeVisible()
// Unconfigured state: no credentials (feed enabled) → the panel says so.
await seedToken('')
await page.goto(`${baseURL}/admin/globals/social-feed-settings`)
await expect(page.getByText('Instagram ainda não configurado')).toBeVisible()
} finally {
await setStub('ok').catch(() => undefined)
await request
.post(`${baseURL}/api/globals/social-feed-settings`, {
headers,
data: {
enabled: true,
youtubeEnabled: true,
youtubeApiKey: '',
youtubeChannelId: '',
youtubeMaxItems: 3,
instagramEnabled: true,
instagramAccessToken: '',
instagramUserId: '',
instagramMaxItems: 3,
excludedItems: [],
youtubeFeedSnapshot: null,
instagramFeedSnapshot: null,
instagramSyncStatus: null,
},
})
.catch(() => undefined)
}
})
})