mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
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)
270 lines
9.8 KiB
TypeScript
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)
|
|
}
|
|
})
|
|
})
|