start-vibing 4.4.0 → 4.4.2

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.
Files changed (58) hide show
  1. package/package.json +2 -2
  2. package/template/.claude/commands/e2e-audit.md +16 -0
  3. package/template/.claude/hooks/e2e-audit-session-start.sh +4 -0
  4. package/template/.claude/hooks/mcp-usage-session-start.sh +4 -0
  5. package/template/.claude/settings.json +8 -0
  6. package/template/.claude/skills/e2e-audit/SKILL.md +216 -660
  7. package/template/.claude/skills/e2e-audit/findings.schema.json +98 -0
  8. package/template/.claude/skills/e2e-audit/references/api-contract-playbook.md +66 -0
  9. package/template/.claude/skills/e2e-audit/references/auth-setup-playbook.md +78 -0
  10. package/template/.claude/skills/e2e-audit/references/coverage-gap-playbook.md +95 -0
  11. package/template/.claude/skills/e2e-audit/references/post-run-feedback-playbook.md +80 -0
  12. package/template/.claude/skills/e2e-audit/scripts/detect-stack.sh +205 -0
  13. package/template/.claude/skills/e2e-audit/scripts/detect-uncovered.sh +137 -0
  14. package/template/.claude/skills/e2e-audit/scripts/discover-api-surface.sh +242 -0
  15. package/template/.claude/skills/e2e-audit/scripts/discover-routes.sh +163 -0
  16. package/template/.claude/skills/e2e-audit/scripts/inventory-existing-tests.sh +161 -0
  17. package/template/.claude/skills/e2e-audit/scripts/verify-audit.sh +88 -0
  18. package/template/.claude/skills/e2e-audit/templates/auth-setup.ts.tpl +24 -0
  19. package/template/.claude/skills/e2e-audit/templates/base-fixture.ts.tpl +75 -0
  20. package/template/.claude/skills/e2e-audit/templates/findings-report.md.tpl +54 -0
  21. package/template/.claude/skills/e2e-audit/templates/post-run-feedback.md.tpl +36 -0
  22. package/template/.claude/skills/e2e-audit/DESIGN.md +0 -294
  23. package/template/.claude/skills/e2e-audit/e2e/fixtures/auth.setup.ts +0 -70
  24. package/template/.claude/skills/e2e-audit/e2e/fixtures/auth.ts +0 -21
  25. package/template/.claude/skills/e2e-audit/e2e/fixtures/base.ts +0 -90
  26. package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/.gitkeep +0 -0
  27. package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/admin.json +0 -50
  28. package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/manager.json +0 -50
  29. package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/member.json +0 -50
  30. package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/owner.json +0 -50
  31. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-admin.page.ts +0 -141
  32. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-billing.page.ts +0 -47
  33. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-chat.page.ts +0 -35
  34. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-home.page.ts +0 -134
  35. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-integrations.page.ts +0 -334
  36. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-knowledge.page.ts +0 -30
  37. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-ontology.page.ts +0 -71
  38. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-profile.page.ts +0 -38
  39. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-teams.page.ts +0 -123
  40. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-transcripts.page.ts +0 -109
  41. package/template/.claude/skills/e2e-audit/e2e/specs/auth/login.spec.ts +0 -59
  42. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-admin.spec.ts +0 -233
  43. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-billing.spec.ts +0 -44
  44. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-chat.spec.ts +0 -50
  45. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-home.spec.ts +0 -243
  46. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-integrations.spec.ts +0 -472
  47. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-knowledge.spec.ts +0 -57
  48. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-ontology.spec.ts +0 -72
  49. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-profile.spec.ts +0 -48
  50. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-teams.spec.ts +0 -247
  51. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-transcripts.spec.ts +0 -122
  52. package/template/.claude/skills/e2e-audit/e2e/specs/security/headers.spec.ts +0 -39
  53. package/template/.claude/skills/e2e-audit/e2e/specs/security/rbac.spec.ts +0 -92
  54. package/template/.claude/skills/e2e-audit/e2e/specs/security/xss.spec.ts +0 -74
  55. package/template/.claude/skills/e2e-audit/e2e/utils/console-collector.ts +0 -89
  56. package/template/.claude/skills/e2e-audit/e2e/utils/security-helpers.ts +0 -114
  57. package/template/.claude/skills/e2e-audit/e2e/utils/test-data.ts +0 -64
  58. package/template/.claude/skills/e2e-audit/runbook.md +0 -115
@@ -1,30 +0,0 @@
1
- import type { Page, Locator } from '@playwright/test'
2
-
3
- export class DashboardKnowledgePage {
4
- readonly page: Page
5
-
6
- readonly heading: Locator
7
- readonly searchInput: Locator
8
- readonly fileList: Locator
9
- readonly emptyState: Locator
10
-
11
- constructor(page: Page) {
12
- this.page = page
13
- this.heading = page.getByRole('heading').first()
14
- this.searchInput = page.getByPlaceholder(/search/i)
15
- this.fileList = page.locator('table, [role="grid"]').first()
16
- this.emptyState = page.getByText(/no files|no knowledge|empty/i)
17
- }
18
-
19
- async goto() {
20
- await this.page.goto('/dashboard/knowledge')
21
- }
22
-
23
- async waitForLoad() {
24
- await this.page.waitForLoadState('networkidle')
25
- }
26
-
27
- async search(query: string) {
28
- await this.searchInput.fill(query)
29
- }
30
- }
@@ -1,71 +0,0 @@
1
- import type { Page, Locator } from '@playwright/test'
2
-
3
- export class DashboardOntologyPage {
4
- readonly page: Page
5
-
6
- // Page header
7
- readonly heading: Locator
8
- readonly description: Locator
9
-
10
- // Stats cards
11
- readonly entitiesStat: Locator
12
- readonly factsStat: Locator
13
- readonly episodesStat: Locator
14
- readonly entityTypesStat: Locator
15
-
16
- // Tabs
17
- readonly graphTab: Locator
18
- readonly entitiesTab: Locator
19
- readonly factsTab: Locator
20
- readonly timelineTab: Locator
21
- readonly searchTab: Locator
22
-
23
- // Tab content
24
- readonly graphEmptyState: Locator
25
- readonly searchInput: Locator
26
-
27
- constructor(page: Page) {
28
- this.page = page
29
-
30
- // Page header
31
- this.heading = page.getByRole('heading', { level: 1 })
32
- this.description = page.getByText(/Explore entidades/i)
33
-
34
- // Stats cards
35
- this.entitiesStat = page.getByText(/Entidades/i).first()
36
- this.factsStat = page.getByText(/Fatos/i).first()
37
- this.episodesStat = page.getByText(/Episódios/i)
38
- this.entityTypesStat = page.getByText(/Tipos de Entidade/i)
39
-
40
- // Tabs
41
- this.graphTab = page.getByRole('tab', { name: /Grafo/i })
42
- this.entitiesTab = page.getByRole('tab', { name: /Entidades/i })
43
- this.factsTab = page.getByRole('tab', { name: /Fatos/i })
44
- this.timelineTab = page.getByRole('tab', { name: /Linha do Tempo/i })
45
- this.searchTab = page.getByRole('tab', { name: /Busca/i })
46
-
47
- // Tab content
48
- this.graphEmptyState = page.getByText(/No data yet/i)
49
- this.searchInput = page.getByRole('textbox', { name: /Search the knowledge graph/i })
50
- }
51
-
52
- async goto(tab?: 'graph' | 'entities' | 'facts' | 'timeline' | 'search') {
53
- const url = tab ? `/dashboard/ontology?tab=${tab}` : '/dashboard/ontology'
54
- await this.page.goto(url)
55
- }
56
-
57
- async waitForLoad() {
58
- await this.heading.waitFor({ timeout: 15000 })
59
- }
60
-
61
- async switchTab(tab: 'graph' | 'entities' | 'facts' | 'timeline' | 'search') {
62
- const tabLocator = {
63
- graph: this.graphTab,
64
- entities: this.entitiesTab,
65
- facts: this.factsTab,
66
- timeline: this.timelineTab,
67
- search: this.searchTab,
68
- }[tab]
69
- await tabLocator.click()
70
- }
71
- }
@@ -1,38 +0,0 @@
1
- import type { Page, Locator } from '@playwright/test'
2
-
3
- export class DashboardProfilePage {
4
- readonly page: Page
5
-
6
- readonly heading: Locator
7
- readonly avatar: Locator
8
- readonly displayName: Locator
9
- readonly displayEmail: Locator
10
-
11
- // Account Info Card
12
- readonly accountCardTitle: Locator
13
- readonly nameInput: Locator
14
- readonly emailInput: Locator
15
- readonly userIdInput: Locator
16
-
17
- constructor(page: Page) {
18
- this.page = page
19
- this.heading = page.getByRole('heading', { level: 1 })
20
- this.avatar = page.getByRole('img', { name: /joão lima/i })
21
- this.displayName = page.getByText('João Lima').first()
22
- this.displayEmail = page.getByText('joao.lima@hakutaku.ai').first()
23
-
24
- // Account Info Card
25
- this.accountCardTitle = page.getByText(/informações da conta|account info/i)
26
- this.nameInput = page.getByRole('textbox', { name: /nome completo|full name/i })
27
- this.emailInput = page.getByRole('textbox', { name: /endereço de e-mail|email address/i })
28
- this.userIdInput = page.getByRole('textbox', { name: /id do usuário|user id/i })
29
- }
30
-
31
- async goto() {
32
- await this.page.goto('/dashboard/profile')
33
- }
34
-
35
- async waitForLoad() {
36
- await this.heading.waitFor({ timeout: 15000 })
37
- }
38
- }
@@ -1,123 +0,0 @@
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
- }
@@ -1,109 +0,0 @@
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
- }
@@ -1,59 +0,0 @@
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
- })
@@ -1,233 +0,0 @@
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
- })