start-vibing 4.3.3 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/template/.claude/agents/sd-audit.md +32 -0
- package/template/.claude/skills/e2e-audit/DESIGN.md +294 -0
- package/template/.claude/skills/e2e-audit/SKILL.md +660 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/auth.setup.ts +70 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/auth.ts +21 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/base.ts +90 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/.gitkeep +0 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/admin.json +50 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/manager.json +50 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/member.json +50 -0
- package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/owner.json +50 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-admin.page.ts +141 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-billing.page.ts +47 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-chat.page.ts +35 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-home.page.ts +134 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-integrations.page.ts +334 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-knowledge.page.ts +30 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-ontology.page.ts +71 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-profile.page.ts +38 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-teams.page.ts +123 -0
- package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-transcripts.page.ts +109 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/auth/login.spec.ts +59 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-admin.spec.ts +233 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-billing.spec.ts +44 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-chat.spec.ts +50 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-home.spec.ts +243 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-integrations.spec.ts +472 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-knowledge.spec.ts +57 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-ontology.spec.ts +72 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-profile.spec.ts +48 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-teams.spec.ts +247 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-transcripts.spec.ts +122 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/security/headers.spec.ts +39 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/security/rbac.spec.ts +92 -0
- package/template/.claude/skills/e2e-audit/e2e/specs/security/xss.spec.ts +74 -0
- package/template/.claude/skills/e2e-audit/e2e/utils/console-collector.ts +89 -0
- package/template/.claude/skills/e2e-audit/e2e/utils/security-helpers.ts +114 -0
- package/template/.claude/skills/e2e-audit/e2e/utils/test-data.ts +64 -0
- package/template/.claude/skills/e2e-audit/runbook.md +115 -0
- package/template/.claude/skills/super-design/SKILL.md +42 -4
- package/template/.claude/skills/super-design/references/audit-methodology.md +63 -7
- package/template/.claude/skills/super-design/scripts/discover-surfaces.sh +197 -0
- package/template/.claude/skills/super-design/scripts/extract-project-rules.sh +240 -0
- package/template/.claude/skills/super-design/scripts/verify-audit.sh +34 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { Page, Locator } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
export class DashboardTeamsPage {
|
|
4
|
+
readonly page: Page
|
|
5
|
+
|
|
6
|
+
// Page Header
|
|
7
|
+
readonly heading: Locator
|
|
8
|
+
readonly subtitle: Locator
|
|
9
|
+
|
|
10
|
+
// Stats Grid
|
|
11
|
+
readonly statDepartments: Locator
|
|
12
|
+
readonly statTeams: Locator
|
|
13
|
+
readonly statMembers: Locator
|
|
14
|
+
readonly statMemberships: Locator
|
|
15
|
+
|
|
16
|
+
// Tabs
|
|
17
|
+
readonly tabMembers: Locator
|
|
18
|
+
readonly tabDepartments: Locator
|
|
19
|
+
readonly tabTeams: Locator
|
|
20
|
+
readonly tabKnowledge: Locator
|
|
21
|
+
|
|
22
|
+
// Members Tab
|
|
23
|
+
readonly addMemberButton: Locator
|
|
24
|
+
readonly memberSearchInput: Locator
|
|
25
|
+
readonly roleFilterCombobox: Locator
|
|
26
|
+
readonly statusFilterCombobox: Locator
|
|
27
|
+
|
|
28
|
+
// Departments Tab
|
|
29
|
+
readonly newDepartmentButton: Locator
|
|
30
|
+
readonly departmentSearchInput: Locator
|
|
31
|
+
readonly departmentsTable: Locator
|
|
32
|
+
|
|
33
|
+
// Teams Tab
|
|
34
|
+
readonly newTeamButton: Locator
|
|
35
|
+
readonly teamSearchInput: Locator
|
|
36
|
+
readonly teamsTable: Locator
|
|
37
|
+
|
|
38
|
+
// Knowledge Tab
|
|
39
|
+
readonly knowledgeTitle: Locator
|
|
40
|
+
readonly addPermissionsButton: Locator
|
|
41
|
+
|
|
42
|
+
constructor(page: Page) {
|
|
43
|
+
this.page = page
|
|
44
|
+
const main = page.getByRole('main')
|
|
45
|
+
|
|
46
|
+
// Page Header
|
|
47
|
+
this.heading = main.getByRole('heading', { name: /equipes e departamentos|teams and departments/i })
|
|
48
|
+
this.subtitle = main.getByText(/gerencie equipes, departamentos e membros/i)
|
|
49
|
+
|
|
50
|
+
// Stats — scoped to main to avoid matching sidebar nav text
|
|
51
|
+
this.statDepartments = main.getByText('Departamentos').first()
|
|
52
|
+
this.statTeams = main.getByText('Equipes').first()
|
|
53
|
+
this.statMembers = main.getByText('Membros').first()
|
|
54
|
+
this.statMemberships = main.getByText(/participações em equipes|team memberships/i)
|
|
55
|
+
|
|
56
|
+
// Tabs
|
|
57
|
+
this.tabMembers = page.getByRole('tab', { name: 'Members' })
|
|
58
|
+
this.tabDepartments = page.getByRole('tab', { name: 'Departments' })
|
|
59
|
+
this.tabTeams = page.getByRole('tab', { name: 'Teams' })
|
|
60
|
+
this.tabKnowledge = page.getByRole('tab', { name: 'Knowledge' })
|
|
61
|
+
|
|
62
|
+
// Members Tab
|
|
63
|
+
this.addMemberButton = page.getByRole('button', { name: /adicionar membro|add member/i })
|
|
64
|
+
this.memberSearchInput = page.getByRole('textbox', { name: /buscar membros|search members/i })
|
|
65
|
+
this.roleFilterCombobox = page.getByRole('combobox').first()
|
|
66
|
+
this.statusFilterCombobox = page.getByRole('combobox').nth(1)
|
|
67
|
+
|
|
68
|
+
// Departments Tab
|
|
69
|
+
this.newDepartmentButton = page.getByRole('button', { name: /novo departamento|new department/i })
|
|
70
|
+
this.departmentSearchInput = page.getByRole('textbox', { name: /buscar departamentos|search departments/i })
|
|
71
|
+
this.departmentsTable = page.getByRole('table')
|
|
72
|
+
|
|
73
|
+
// Teams Tab
|
|
74
|
+
this.newTeamButton = page.getByRole('button', { name: /nova equipe|new team/i })
|
|
75
|
+
this.teamSearchInput = page.getByRole('textbox', { name: /buscar equipes|search teams/i })
|
|
76
|
+
this.teamsTable = page.getByRole('table')
|
|
77
|
+
|
|
78
|
+
// Knowledge Tab
|
|
79
|
+
this.knowledgeTitle = page.getByText(/permissões de conhecimento|knowledge permissions/i)
|
|
80
|
+
this.addPermissionsButton = page.getByRole('button', { name: /adicionar permissões|add permissions/i })
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async goto() {
|
|
84
|
+
await this.page.goto('/dashboard/teams')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async waitForLoad() {
|
|
88
|
+
await this.heading.waitFor({ timeout: 30000 })
|
|
89
|
+
// Wait for member data to load (first h4 heading = member card)
|
|
90
|
+
await this.page.getByRole('heading', { level: 4 }).first().waitFor({ timeout: 15000 })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async switchToTab(tab: 'members' | 'departments' | 'teams' | 'knowledge') {
|
|
94
|
+
const tabMap = {
|
|
95
|
+
members: this.tabMembers,
|
|
96
|
+
departments: this.tabDepartments,
|
|
97
|
+
teams: this.tabTeams,
|
|
98
|
+
knowledge: this.tabKnowledge,
|
|
99
|
+
}
|
|
100
|
+
await tabMap[tab].click()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async selectRoleFilter(role: string) {
|
|
104
|
+
await this.roleFilterCombobox.click()
|
|
105
|
+
await this.page.getByRole('option', { name: role }).click()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async selectStatusFilter(status: string) {
|
|
109
|
+
await this.statusFilterCombobox.click()
|
|
110
|
+
await this.page.getByRole('option', { name: status }).click()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async clickMemberByName(name: string) {
|
|
114
|
+
await this.page.getByRole('heading', { level: 4, name }).click()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async openRowMenu(index = 0) {
|
|
118
|
+
const buttons = await this.page.getByRole('button', { name: /abrir menu|open menu/i }).all()
|
|
119
|
+
if (buttons[index]) {
|
|
120
|
+
await buttons[index].click()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { Page, Locator } from '@playwright/test'
|
|
2
|
+
|
|
3
|
+
export class DashboardTranscriptsPage {
|
|
4
|
+
readonly page: Page
|
|
5
|
+
|
|
6
|
+
// Page header
|
|
7
|
+
readonly heading: Locator
|
|
8
|
+
readonly description: Locator
|
|
9
|
+
readonly windowsDownloadLink: Locator
|
|
10
|
+
readonly macDownloadLink: Locator
|
|
11
|
+
|
|
12
|
+
// Stats cards
|
|
13
|
+
readonly totalTranscriptsStat: Locator
|
|
14
|
+
readonly totalDurationStat: Locator
|
|
15
|
+
readonly documentsStat: Locator
|
|
16
|
+
readonly conflictsStat: Locator
|
|
17
|
+
|
|
18
|
+
// Tabs
|
|
19
|
+
readonly meetingsTab: Locator
|
|
20
|
+
readonly documentsTab: Locator
|
|
21
|
+
readonly conflictsTab: Locator
|
|
22
|
+
|
|
23
|
+
// --- Meetings Tab ---
|
|
24
|
+
readonly meetingsSearchInput: Locator
|
|
25
|
+
readonly meetingsStatusFilter: Locator
|
|
26
|
+
readonly meetingsTable: Locator
|
|
27
|
+
readonly paginationText: Locator
|
|
28
|
+
readonly paginationPrevButton: Locator
|
|
29
|
+
readonly paginationNextButton: Locator
|
|
30
|
+
|
|
31
|
+
// --- Documents Tab ---
|
|
32
|
+
readonly documentsEmptyHeading: Locator
|
|
33
|
+
|
|
34
|
+
// --- Conflicts Tab ---
|
|
35
|
+
readonly conflictsHeading: Locator
|
|
36
|
+
readonly conflictsEmptyHeading: Locator
|
|
37
|
+
|
|
38
|
+
// --- Detail Page ---
|
|
39
|
+
readonly detailBackButton: Locator
|
|
40
|
+
readonly detailHeading: Locator
|
|
41
|
+
readonly detailStatusBadge: Locator
|
|
42
|
+
|
|
43
|
+
constructor(page: Page) {
|
|
44
|
+
this.page = page
|
|
45
|
+
|
|
46
|
+
// Page header
|
|
47
|
+
this.heading = page.getByRole('heading', { level: 1 })
|
|
48
|
+
this.description = page.getByText(/Visualize transcrições/i)
|
|
49
|
+
this.windowsDownloadLink = page.getByRole('link', { name: /Windows/i })
|
|
50
|
+
this.macDownloadLink = page.getByRole('link', { name: /macOS/i })
|
|
51
|
+
|
|
52
|
+
// Stats cards
|
|
53
|
+
this.totalTranscriptsStat = page.getByText(/Total de Transcrições/i)
|
|
54
|
+
this.totalDurationStat = page.getByText(/Duração Total/i)
|
|
55
|
+
this.documentsStat = page.getByText(/Documentos/i).first()
|
|
56
|
+
this.conflictsStat = page.getByText(/Conflitos Ativos/i)
|
|
57
|
+
|
|
58
|
+
// Tabs
|
|
59
|
+
this.meetingsTab = page.getByRole('tab', { name: /Reuniões/i })
|
|
60
|
+
this.documentsTab = page.getByRole('tab', { name: /Documentos/i })
|
|
61
|
+
this.conflictsTab = page.getByRole('tab', { name: /Conflitos/i })
|
|
62
|
+
|
|
63
|
+
// --- Meetings Tab ---
|
|
64
|
+
this.meetingsSearchInput = page.getByRole('textbox', { name: /Buscar transcrições/i })
|
|
65
|
+
this.meetingsStatusFilter = page.getByRole('combobox')
|
|
66
|
+
this.meetingsTable = page.getByRole('table')
|
|
67
|
+
this.paginationText = page.getByText(/Exibindo \d+ - \d+ de \d+/)
|
|
68
|
+
this.paginationPrevButton = page.locator('button:has(img)').filter({ hasText: '' }).nth(-2)
|
|
69
|
+
this.paginationNextButton = page.locator('button:has(img)').filter({ hasText: '' }).last()
|
|
70
|
+
|
|
71
|
+
// --- Documents Tab ---
|
|
72
|
+
this.documentsEmptyHeading = page.getByRole('heading', { name: /Nenhum documento/i })
|
|
73
|
+
|
|
74
|
+
// --- Conflicts Tab ---
|
|
75
|
+
this.conflictsHeading = page.getByRole('heading', { name: /Todos os Conflitos/i })
|
|
76
|
+
this.conflictsEmptyHeading = page.getByRole('heading', { name: /Nenhum conflito/i })
|
|
77
|
+
|
|
78
|
+
// --- Detail Page ---
|
|
79
|
+
this.detailBackButton = page.locator('main button').first()
|
|
80
|
+
this.detailHeading = page.getByRole('heading', { level: 1 })
|
|
81
|
+
this.detailStatusBadge = page.getByText(/Concluído|Pendente|Processando|Falhou/i).first()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async goto(tab?: 'meetings' | 'documents' | 'conflicts') {
|
|
85
|
+
const url = tab ? `/dashboard/transcripts?tab=${tab}` : '/dashboard/transcripts'
|
|
86
|
+
await this.page.goto(url)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async waitForLoad() {
|
|
90
|
+
await this.heading.waitFor({ timeout: 15000 })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async switchTab(tab: 'meetings' | 'documents' | 'conflicts') {
|
|
94
|
+
const tabLocator = {
|
|
95
|
+
meetings: this.meetingsTab,
|
|
96
|
+
documents: this.documentsTab,
|
|
97
|
+
conflicts: this.conflictsTab,
|
|
98
|
+
}[tab]
|
|
99
|
+
await tabLocator.click()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async clickTranscriptRow(titleSubstring: string) {
|
|
103
|
+
await this.page.getByRole('row').filter({ hasText: titleSubstring }).first().click()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async getMeetingRowCount() {
|
|
107
|
+
return this.meetingsTable.getByRole('row').count() - 1 // subtract header row
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { test, expect } from '@playwright/test'
|
|
2
|
+
import { ROUTES } from '../../utils/test-data'
|
|
3
|
+
|
|
4
|
+
test.describe('Auth - Login Page @smoke', () => {
|
|
5
|
+
test('landing page loads', async ({ page }) => {
|
|
6
|
+
await page.goto(ROUTES.root)
|
|
7
|
+
await page.waitForLoadState('networkidle')
|
|
8
|
+
// Should show login/landing page
|
|
9
|
+
await expect(page).toHaveURL(ROUTES.root)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('has sign-in options visible', async ({ page }) => {
|
|
13
|
+
await page.goto(ROUTES.root)
|
|
14
|
+
await page.waitForLoadState('networkidle')
|
|
15
|
+
|
|
16
|
+
// Check for OAuth buttons
|
|
17
|
+
const googleBtn = page.getByRole('button', { name: /google/i })
|
|
18
|
+
const microsoftBtn = page.getByRole('button', { name: /microsoft/i })
|
|
19
|
+
|
|
20
|
+
const hasGoogle = await googleBtn.isVisible().catch(() => false)
|
|
21
|
+
const hasMicrosoft = await microsoftBtn.isVisible().catch(() => false)
|
|
22
|
+
|
|
23
|
+
// At least one auth provider should be visible
|
|
24
|
+
expect(hasGoogle || hasMicrosoft).toBe(true)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test.describe('Auth - Error Page @smoke', () => {
|
|
29
|
+
test('auth error page loads', async ({ page }) => {
|
|
30
|
+
await page.goto(ROUTES.authError)
|
|
31
|
+
await page.waitForLoadState('networkidle')
|
|
32
|
+
// Should display some error content
|
|
33
|
+
const heading = page.getByRole('heading').first()
|
|
34
|
+
await expect(heading).toBeVisible()
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test.describe('Auth - Unauthorized Page @smoke', () => {
|
|
39
|
+
test('unauthorized page loads', async ({ page }) => {
|
|
40
|
+
await page.goto(ROUTES.unauthorized)
|
|
41
|
+
await page.waitForLoadState('networkidle')
|
|
42
|
+
const heading = page.getByRole('heading').first()
|
|
43
|
+
await expect(heading).toBeVisible()
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test.describe('Auth - Logout @smoke', () => {
|
|
48
|
+
test('logout page redirects', async ({ page }) => {
|
|
49
|
+
await page.goto(ROUTES.logout)
|
|
50
|
+
await page.waitForLoadState('networkidle')
|
|
51
|
+
// Should redirect to login or show logout confirmation
|
|
52
|
+
const url = page.url()
|
|
53
|
+
const handled =
|
|
54
|
+
url.includes('/auth') ||
|
|
55
|
+
url === 'http://localhost:3000/' ||
|
|
56
|
+
url.includes('/logout')
|
|
57
|
+
expect(handled).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { test, expect } from '../fixtures/base'
|
|
2
|
+
import { DashboardAdminPage } from '../pages/dashboard-admin.page'
|
|
3
|
+
import { createConsoleCollector } from '../utils/console-collector'
|
|
4
|
+
|
|
5
|
+
test.describe('Dashboard Admin @smoke', () => {
|
|
6
|
+
let admin: DashboardAdminPage
|
|
7
|
+
|
|
8
|
+
test.beforeEach(async ({ authenticatedPage, apiErrors: _apiErrors }) => {
|
|
9
|
+
admin = new DashboardAdminPage(authenticatedPage)
|
|
10
|
+
await admin.goto()
|
|
11
|
+
await admin.waitForLoad()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('page loads with heading, badge, and welcome text', async ({ authenticatedPage }) => {
|
|
15
|
+
await expect(authenticatedPage, 'Should navigate to admin page').toHaveURL(/dashboard\/admin/)
|
|
16
|
+
await expect(admin.heading, 'Admin heading should be visible').toBeVisible()
|
|
17
|
+
await expect(admin.adminBadge).toBeVisible()
|
|
18
|
+
await expect(admin.welcomeText).toBeVisible()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('shows three tabs', async () => {
|
|
22
|
+
await expect(admin.organizationsTab).toBeVisible()
|
|
23
|
+
await expect(admin.usersTab).toBeVisible()
|
|
24
|
+
await expect(admin.systemOverviewTab).toBeVisible()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('organizations tab is selected by default', async () => {
|
|
28
|
+
await expect(admin.organizationsTab).toHaveAttribute('aria-selected', 'true')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('tabs switch via URL', async ({ authenticatedPage }) => {
|
|
32
|
+
await admin.switchTab('users')
|
|
33
|
+
await expect(authenticatedPage).toHaveURL(/tab=users/)
|
|
34
|
+
await expect(admin.usersHeading).toBeVisible()
|
|
35
|
+
|
|
36
|
+
await admin.switchTab('system')
|
|
37
|
+
await expect(authenticatedPage).toHaveURL(/tab=system/)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test.describe('Dashboard Admin - Organizations Tab', () => {
|
|
42
|
+
let admin: DashboardAdminPage
|
|
43
|
+
|
|
44
|
+
test.beforeEach(async ({ authenticatedPage, apiErrors: _apiErrors }) => {
|
|
45
|
+
admin = new DashboardAdminPage(authenticatedPage)
|
|
46
|
+
await admin.goto('organizations')
|
|
47
|
+
await admin.waitForLoad()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test('displays org management heading and controls', async () => {
|
|
51
|
+
await expect(admin.orgHeading).toBeVisible()
|
|
52
|
+
await expect(admin.orgSearchInput).toBeVisible()
|
|
53
|
+
await expect(admin.newOrgButton).toBeVisible()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('org table has rows with data', async () => {
|
|
57
|
+
await admin.orgTable.waitFor()
|
|
58
|
+
const rowCount = await admin.getOrgRowCount()
|
|
59
|
+
expect(rowCount).toBeGreaterThan(0)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('clicking org row opens detail dialog', async () => {
|
|
63
|
+
await admin.orgTable.waitFor()
|
|
64
|
+
await admin.clickOrgRow('Hakutaku')
|
|
65
|
+
await expect(admin.orgDetailDialog).toBeVisible()
|
|
66
|
+
await expect(admin.orgDetailOverviewTab).toBeVisible()
|
|
67
|
+
await expect(admin.orgDetailMembersTab).toBeVisible()
|
|
68
|
+
await expect(admin.orgDetailSettingsTab).toBeVisible()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('org detail overview tab shows info sections', async () => {
|
|
72
|
+
await admin.orgTable.waitFor()
|
|
73
|
+
await admin.clickOrgRow('Hakutaku')
|
|
74
|
+
await expect(admin.orgDetailDialog).toBeVisible()
|
|
75
|
+
await expect(admin.orgDetailEditButton).toBeVisible()
|
|
76
|
+
await expect(admin.orgDetailDialog.getByText(/Informações da Organização/i)).toBeVisible()
|
|
77
|
+
await expect(admin.orgDetailDialog.getByText(/Membros/i).first()).toBeVisible()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('org detail members tab shows member list', async () => {
|
|
81
|
+
await admin.orgTable.waitFor()
|
|
82
|
+
await admin.clickOrgRow('Hakutaku')
|
|
83
|
+
await admin.orgDetailMembersTab.click()
|
|
84
|
+
await expect(admin.orgDetailDialog.getByText(/Membros da Equipe/i)).toBeVisible()
|
|
85
|
+
await expect(admin.orgDetailDialog.getByRole('button', { name: /Convidar Membro/i })).toBeVisible()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('org detail settings tab shows danger zone', async () => {
|
|
89
|
+
await admin.orgTable.waitFor()
|
|
90
|
+
await admin.clickOrgRow('Hakutaku')
|
|
91
|
+
await admin.orgDetailSettingsTab.click()
|
|
92
|
+
await expect(admin.orgDetailDialog.getByText(/Zona de Perigo/i)).toBeVisible()
|
|
93
|
+
await expect(admin.orgDetailDeleteButton).toBeVisible()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('new organization button opens wizard dialog', async () => {
|
|
97
|
+
await admin.newOrgButton.click()
|
|
98
|
+
await expect(admin.newOrgNameInput).toBeVisible()
|
|
99
|
+
await expect(admin.newOrgSubdomainInput).toBeDisabled()
|
|
100
|
+
await expect(admin.newOrgPrevButton).toBeDisabled()
|
|
101
|
+
await expect(admin.newOrgNextButton).toBeDisabled()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('search filters organizations', async () => {
|
|
105
|
+
await admin.orgSearchInput.fill('Hakutaku')
|
|
106
|
+
await expect(admin.orgTable.getByText('Hakutaku').first()).toBeVisible()
|
|
107
|
+
const rowCount = await admin.getOrgRowCount()
|
|
108
|
+
expect(rowCount).toBe(1)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('new org wizard advances through all 5 steps', async ({ authenticatedPage }) => {
|
|
112
|
+
await admin.newOrgButton.click()
|
|
113
|
+
|
|
114
|
+
// Step 1: fill name, subdomain auto-fills
|
|
115
|
+
await admin.newOrgNameInput.fill('E2E Wizard Test')
|
|
116
|
+
await expect(admin.newOrgSubdomainInput).toHaveValue('e2e-wizard-test')
|
|
117
|
+
await expect(admin.newOrgNextButton).toBeEnabled()
|
|
118
|
+
await admin.newOrgNextButton.click()
|
|
119
|
+
|
|
120
|
+
// Step 2: Configurações (timezone pre-selected)
|
|
121
|
+
await expect(authenticatedPage.getByText(/Configurações da Organização/i)).toBeVisible()
|
|
122
|
+
await expect(admin.newOrgNextButton).toBeEnabled()
|
|
123
|
+
await admin.newOrgNextButton.click()
|
|
124
|
+
|
|
125
|
+
// Step 3: Proprietário (fill email)
|
|
126
|
+
await expect(authenticatedPage.getByText(/Informações do Proprietário/i)).toBeVisible()
|
|
127
|
+
const ownerEmail = authenticatedPage.getByRole('textbox', { name: /E-mail do Proprietário/i })
|
|
128
|
+
await ownerEmail.fill('wizard-test@hakutaku.ai')
|
|
129
|
+
await expect(admin.newOrgNextButton).toBeEnabled()
|
|
130
|
+
await admin.newOrgNextButton.click()
|
|
131
|
+
|
|
132
|
+
// Step 4: Recursos (defaults pre-filled)
|
|
133
|
+
await expect(authenticatedPage.getByText(/Limites de Recursos/i)).toBeVisible()
|
|
134
|
+
await expect(admin.newOrgNextButton).toBeEnabled()
|
|
135
|
+
await admin.newOrgNextButton.click()
|
|
136
|
+
|
|
137
|
+
// Step 5: Revisão (final step with create button)
|
|
138
|
+
await expect(authenticatedPage.getByText(/Assinatura e Status/i)).toBeVisible()
|
|
139
|
+
const createButton = authenticatedPage.getByRole('button', { name: /Criar Organização/i })
|
|
140
|
+
await expect(createButton).toBeVisible()
|
|
141
|
+
|
|
142
|
+
// Close without creating
|
|
143
|
+
await admin.closeDialog()
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
test.describe('Dashboard Admin - Users Tab', () => {
|
|
148
|
+
let admin: DashboardAdminPage
|
|
149
|
+
|
|
150
|
+
test.beforeEach(async ({ authenticatedPage, apiErrors: _apiErrors }) => {
|
|
151
|
+
admin = new DashboardAdminPage(authenticatedPage)
|
|
152
|
+
await admin.goto('users')
|
|
153
|
+
await admin.waitForLoad()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('displays user management heading and controls', async () => {
|
|
157
|
+
await expect(admin.usersHeading).toBeVisible()
|
|
158
|
+
await expect(admin.usersSearchInput).toBeVisible()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
test('users table has rows with data', async () => {
|
|
162
|
+
await admin.usersTable.waitFor()
|
|
163
|
+
const rowCount = await admin.getUserRowCount()
|
|
164
|
+
expect(rowCount).toBeGreaterThan(0)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('clicking user row opens edit dialog', async () => {
|
|
168
|
+
await admin.usersTable.waitFor()
|
|
169
|
+
await admin.clickUserRow('João Lima')
|
|
170
|
+
await expect(admin.userEditNameInput).toBeVisible()
|
|
171
|
+
await expect(admin.userEditStatusCombobox).toBeVisible()
|
|
172
|
+
await expect(admin.userEditSaveButton).toBeVisible()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
test('user edit dialog pre-fills user name', async () => {
|
|
176
|
+
await admin.usersTable.waitFor()
|
|
177
|
+
await admin.clickUserRow('João Lima')
|
|
178
|
+
await expect(admin.userEditNameInput).toHaveValue('João Lima')
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('search filters users', async () => {
|
|
182
|
+
await admin.usersSearchInput.fill('João')
|
|
183
|
+
await expect(admin.usersTable.getByText('João Lima').first()).toBeVisible()
|
|
184
|
+
const rowCount = await admin.getUserRowCount()
|
|
185
|
+
expect(rowCount).toBe(1)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
test.describe('Dashboard Admin - System Overview Tab', () => {
|
|
190
|
+
let admin: DashboardAdminPage
|
|
191
|
+
|
|
192
|
+
test.beforeEach(async ({ authenticatedPage, apiErrors: _apiErrors }) => {
|
|
193
|
+
admin = new DashboardAdminPage(authenticatedPage)
|
|
194
|
+
await admin.goto('system')
|
|
195
|
+
await admin.waitForLoad()
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
test('displays system overview heading', async () => {
|
|
199
|
+
await expect(admin.systemHeading).toBeVisible()
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
test('shows error state with retry button (known bug)', async () => {
|
|
203
|
+
// BUG-ADMIN-001: useAdminStats passes take:1000, Zod limits to <=100
|
|
204
|
+
await expect(admin.systemErrorHeading).toBeVisible()
|
|
205
|
+
await expect(admin.systemRetryButton).toBeVisible()
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
test.describe('Dashboard Admin - Security @security', () => {
|
|
210
|
+
test('no sensitive data in console on organizations tab', async ({ authenticatedPage, apiErrors: _apiErrors }) => {
|
|
211
|
+
const collector = createConsoleCollector(authenticatedPage)
|
|
212
|
+
const admin = new DashboardAdminPage(authenticatedPage)
|
|
213
|
+
await admin.goto('organizations')
|
|
214
|
+
await admin.waitForLoad()
|
|
215
|
+
collector.assertNoSensitiveLeaks()
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
test('XSS in user names is escaped', async ({ authenticatedPage, apiErrors: _apiErrors }) => {
|
|
219
|
+
const admin = new DashboardAdminPage(authenticatedPage)
|
|
220
|
+
await admin.goto('users')
|
|
221
|
+
await admin.waitForLoad()
|
|
222
|
+
await admin.usersTable.waitFor()
|
|
223
|
+
|
|
224
|
+
// Verify XSS test data is displayed as escaped text
|
|
225
|
+
const xssRow = authenticatedPage.getByRole('row').filter({ hasText: 'xss@test.com' })
|
|
226
|
+
await expect(xssRow).toBeVisible()
|
|
227
|
+
|
|
228
|
+
// The script tag should be rendered as text, not executed
|
|
229
|
+
const cellText = await xssRow.getByRole('gridcell').first().textContent()
|
|
230
|
+
expect(cellText).toContain('<script>')
|
|
231
|
+
expect(cellText).not.toContain('[object')
|
|
232
|
+
})
|
|
233
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { test, expect } from '../fixtures/base'
|
|
2
|
+
import { DashboardBillingPage } from '../pages/dashboard-billing.page'
|
|
3
|
+
import { createConsoleCollector } from '../utils/console-collector'
|
|
4
|
+
|
|
5
|
+
test.describe('Dashboard Billing @smoke', () => {
|
|
6
|
+
let billing: DashboardBillingPage
|
|
7
|
+
|
|
8
|
+
test.beforeEach(async ({ authenticatedPage, apiErrors: _apiErrors }) => {
|
|
9
|
+
billing = new DashboardBillingPage(authenticatedPage)
|
|
10
|
+
await billing.goto()
|
|
11
|
+
await billing.waitForLoad()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
test('page loads with heading and description', async ({ authenticatedPage }) => {
|
|
15
|
+
await expect(authenticatedPage, 'Should navigate to billing page').toHaveURL(/dashboard\/billing/)
|
|
16
|
+
await expect(billing.heading, 'Billing heading should be visible').toBeVisible()
|
|
17
|
+
await expect(billing.description).toBeVisible()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('shows subscription card with plan info', async () => {
|
|
21
|
+
await expect(billing.planName).toBeVisible()
|
|
22
|
+
await expect(billing.planLabel).toBeVisible()
|
|
23
|
+
await expect(billing.activeUsersLabel).toBeVisible()
|
|
24
|
+
await expect(billing.statusLabel).toBeVisible()
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('shows subscribe button', async () => {
|
|
28
|
+
await expect(billing.subscribeButton).toBeVisible()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('shows Stripe security text', async () => {
|
|
32
|
+
await expect(billing.stripeSecurityText).toBeVisible()
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test.describe('Dashboard Billing - Security @security', () => {
|
|
37
|
+
test('no sensitive data in console', async ({ authenticatedPage, apiErrors: _apiErrors }) => {
|
|
38
|
+
const collector = createConsoleCollector(authenticatedPage)
|
|
39
|
+
const billing = new DashboardBillingPage(authenticatedPage)
|
|
40
|
+
await billing.goto()
|
|
41
|
+
await billing.waitForLoad()
|
|
42
|
+
collector.assertNoSensitiveLeaks()
|
|
43
|
+
})
|
|
44
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { test, expect } from '../fixtures/base'
|
|
2
|
+
import { DashboardChatPage } from '../pages/dashboard-chat.page'
|
|
3
|
+
import { createConsoleCollector } from '../utils/console-collector'
|
|
4
|
+
import { testXssOnInput } from '../utils/security-helpers'
|
|
5
|
+
|
|
6
|
+
test.describe('Dashboard Chat @smoke', () => {
|
|
7
|
+
let chat: DashboardChatPage
|
|
8
|
+
|
|
9
|
+
test.beforeEach(async ({ authenticatedPage, apiErrors: _apiErrors }) => {
|
|
10
|
+
chat = new DashboardChatPage(authenticatedPage)
|
|
11
|
+
await chat.goto()
|
|
12
|
+
await chat.waitForLoad()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('page loads successfully', async ({ authenticatedPage }) => {
|
|
16
|
+
await expect(authenticatedPage, 'Should navigate to chat page').toHaveURL(/dashboard\/chat/)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('displays page heading', async () => {
|
|
20
|
+
await expect(chat.heading).toBeVisible()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('message input is visible and enabled', async () => {
|
|
24
|
+
const inputVisible = await chat.messageInput.isVisible().catch(() => false)
|
|
25
|
+
if (inputVisible) {
|
|
26
|
+
await expect(chat.messageInput).toBeEnabled()
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test.describe('Dashboard Chat - Security @security', () => {
|
|
32
|
+
test('no sensitive data in console', async ({ authenticatedPage, apiErrors: _apiErrors }) => {
|
|
33
|
+
const collector = createConsoleCollector(authenticatedPage)
|
|
34
|
+
const chat = new DashboardChatPage(authenticatedPage)
|
|
35
|
+
await chat.goto()
|
|
36
|
+
await chat.waitForLoad()
|
|
37
|
+
collector.assertNoSensitiveLeaks()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('message input resists XSS', async ({ authenticatedPage, apiErrors: _apiErrors }) => {
|
|
41
|
+
const chat = new DashboardChatPage(authenticatedPage)
|
|
42
|
+
await chat.goto()
|
|
43
|
+
await chat.waitForLoad()
|
|
44
|
+
|
|
45
|
+
const inputVisible = await chat.messageInput.isVisible().catch(() => false)
|
|
46
|
+
if (inputVisible) {
|
|
47
|
+
await testXssOnInput(authenticatedPage, chat.messageInput)
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
})
|