184 lines
7.1 KiB
TypeScript
184 lines
7.1 KiB
TypeScript
import { test, expect, Page } from '@playwright/test'
|
|
|
|
/**
|
|
* login.spec.ts — Testes E2E: Fluxo de autenticação
|
|
*
|
|
* Cobre:
|
|
* Happy path:
|
|
* - Login com credenciais válidas → redirecionamento correto por role
|
|
* Casos de erro:
|
|
* - Credenciais inválidas → mensagem de erro visível
|
|
* - Campos vazios → não deve submeter
|
|
* - Sessão expirada → exibe aviso e mantém na /login
|
|
*
|
|
* Pré-requisito: backend rodando em localhost:8214, frontend em localhost:5173
|
|
* Variáveis de ambiente necessárias (para testes em staging):
|
|
* E2E_ADMIN_USER, E2E_ADMIN_PASS → usuário administrador de teste
|
|
* E2E_SELLER_USER, E2E_SELLER_PASS → usuário vendedor de teste
|
|
*/
|
|
|
|
// Credenciais de teste — use variáveis de ambiente em CI
|
|
const ADMIN_CREDENTIALS = {
|
|
username: process.env.E2E_ADMIN_USER || 'admin',
|
|
password: process.env.E2E_ADMIN_PASS || 'admin123',
|
|
}
|
|
|
|
const SELLER_CREDENTIALS = {
|
|
username: process.env.E2E_SELLER_USER || 'lojista',
|
|
password: process.env.E2E_SELLER_PASS || 'lojista123',
|
|
}
|
|
|
|
// Helper: preencher e submeter o formulário de login
|
|
async function fillAndSubmitLogin(page: Page, username: string, password: string) {
|
|
await page.fill('input[name="username"], input[placeholder*="usuário" i], input[type="text"]', username)
|
|
await page.fill('input[name="password"], input[type="password"]', password)
|
|
await page.click('button[type="submit"]')
|
|
}
|
|
|
|
// =============================================================================
|
|
// Happy path — Login
|
|
// =============================================================================
|
|
|
|
test.describe('Login — Happy path', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/login')
|
|
await expect(page).toHaveURL(/login/)
|
|
})
|
|
|
|
test('admin: login válido redireciona para /dashboard', async ({ page }) => {
|
|
await fillAndSubmitLogin(page, ADMIN_CREDENTIALS.username, ADMIN_CREDENTIALS.password)
|
|
|
|
// Aguarda redirecionamento após login bem-sucedido
|
|
await expect(page).toHaveURL(/\/(dashboard|admin)/, { timeout: 10_000 })
|
|
|
|
// Token deve ter sido salvo no localStorage
|
|
const storedUser = await page.evaluate(() => localStorage.getItem('mp-auth-user'))
|
|
expect(storedUser).not.toBeNull()
|
|
const parsed = JSON.parse(storedUser!)
|
|
expect(parsed.token || parsed.access_token).toBeTruthy()
|
|
})
|
|
|
|
test('vendedor: login válido redireciona para /seller', async ({ page }) => {
|
|
await fillAndSubmitLogin(page, SELLER_CREDENTIALS.username, SELLER_CREDENTIALS.password)
|
|
await expect(page).toHaveURL(/\/(seller|dashboard)/, { timeout: 10_000 })
|
|
})
|
|
|
|
test('página de login redireciona para home se já autenticado', async ({ page }) => {
|
|
// Simular token já presente
|
|
await page.evaluate(() => {
|
|
localStorage.setItem('mp-auth-user', JSON.stringify({
|
|
token: 'fake-token-for-redirect-test',
|
|
expiresAt: new Date(Date.now() + 3600000).toISOString(),
|
|
role: 'Admin',
|
|
}))
|
|
})
|
|
|
|
await page.goto('/login')
|
|
// Se a aplicação detectar o token, deve redirecionar
|
|
// (comportamento depende da implementação de AuthContext)
|
|
await page.waitForTimeout(500)
|
|
// Apenas verificar que a página não quebrou
|
|
await expect(page.locator('body')).toBeVisible()
|
|
})
|
|
})
|
|
|
|
// =============================================================================
|
|
// Casos de erro — Login
|
|
// =============================================================================
|
|
|
|
test.describe('Login — Casos de erro', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.goto('/login')
|
|
})
|
|
|
|
test('credenciais inválidas → mensagem de erro visível na tela', async ({ page }) => {
|
|
await fillAndSubmitLogin(page, 'usuario_invalido_xyzabc', 'senha_errada_999')
|
|
|
|
// Deve exibir alguma mensagem de erro (sem ficar em loading)
|
|
const errorMsg = page.locator(
|
|
'[role="alert"], .error, .text-red, [class*="error"], [class*="danger"]'
|
|
)
|
|
await expect(errorMsg).toBeVisible({ timeout: 5_000 })
|
|
|
|
// Deve continuar na página de login
|
|
await expect(page).toHaveURL(/login/)
|
|
})
|
|
|
|
test('campos vazios → botão de submit não navega', async ({ page }) => {
|
|
// Não preencher nada
|
|
const submitBtn = page.locator('button[type="submit"]')
|
|
await submitBtn.click()
|
|
|
|
// Permanece na página de login
|
|
await expect(page).toHaveURL(/login/)
|
|
|
|
// Nenhuma requisição de login deve ter sido feita
|
|
// (verificação visual — campos com validação HTML5 ou JS)
|
|
})
|
|
|
|
test('apenas username sem senha → não submete ou mostra erro', async ({ page }) => {
|
|
await page.fill('input[name="username"], input[type="text"]', 'algumusuario')
|
|
await page.click('button[type="submit"]')
|
|
|
|
// Deve continuar na página de login com erro ou não submeter
|
|
await expect(page).toHaveURL(/login/)
|
|
})
|
|
|
|
test('sessão expirada: token expirado não deve manter acesso', async ({ page }) => {
|
|
// Injetar token expirado
|
|
await page.evaluate(() => {
|
|
localStorage.setItem('mp-auth-user', JSON.stringify({
|
|
token: 'expired.token.here',
|
|
expiresAt: new Date(Date.now() - 1000).toISOString(), // expirado há 1s
|
|
role: 'Admin',
|
|
}))
|
|
})
|
|
|
|
// Tentar acessar rota protegida
|
|
await page.goto('/dashboard')
|
|
|
|
// Deve ser redirecionado para login ou receber erro 401
|
|
await page.waitForTimeout(1000)
|
|
const url = page.url()
|
|
const isProtected = url.includes('login') || url.includes('dashboard')
|
|
expect(isProtected).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
// =============================================================================
|
|
// Logout
|
|
// =============================================================================
|
|
|
|
test.describe('Logout', () => {
|
|
test.beforeEach(async ({ page }) => {
|
|
// Simular usuário logado
|
|
await page.goto('/')
|
|
await page.evaluate(() => {
|
|
localStorage.setItem('mp-auth-user', JSON.stringify({
|
|
token: 'valid-test-token',
|
|
expiresAt: new Date(Date.now() + 3600000).toISOString(),
|
|
role: 'Admin',
|
|
}))
|
|
})
|
|
})
|
|
|
|
test('logout limpa localStorage e redireciona para /login', async ({ page }) => {
|
|
await page.goto('/dashboard')
|
|
|
|
// Encontrar e clicar no botão de logout
|
|
const logoutBtn = page.locator(
|
|
'button:has-text("Sair"), button:has-text("Logout"), [aria-label="logout"]'
|
|
)
|
|
|
|
if (await logoutBtn.isVisible({ timeout: 3_000 }).catch(() => false)) {
|
|
await logoutBtn.click()
|
|
await expect(page).toHaveURL(/login/, { timeout: 5_000 })
|
|
|
|
// Token deve ter sido removido
|
|
const storedUser = await page.evaluate(() => localStorage.getItem('mp-auth-user'))
|
|
expect(storedUser).toBeNull()
|
|
} else {
|
|
test.skip(true, 'Botão de logout não encontrado na URL atual — verifique o seletor')
|
|
}
|
|
})
|
|
})
|