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.
Files changed (45) hide show
  1. package/package.json +2 -2
  2. package/template/.claude/agents/sd-audit.md +32 -0
  3. package/template/.claude/skills/e2e-audit/DESIGN.md +294 -0
  4. package/template/.claude/skills/e2e-audit/SKILL.md +660 -0
  5. package/template/.claude/skills/e2e-audit/e2e/fixtures/auth.setup.ts +70 -0
  6. package/template/.claude/skills/e2e-audit/e2e/fixtures/auth.ts +21 -0
  7. package/template/.claude/skills/e2e-audit/e2e/fixtures/base.ts +90 -0
  8. package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/.gitkeep +0 -0
  9. package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/admin.json +50 -0
  10. package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/manager.json +50 -0
  11. package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/member.json +50 -0
  12. package/template/.claude/skills/e2e-audit/e2e/fixtures/storage/owner.json +50 -0
  13. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-admin.page.ts +141 -0
  14. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-billing.page.ts +47 -0
  15. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-chat.page.ts +35 -0
  16. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-home.page.ts +134 -0
  17. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-integrations.page.ts +334 -0
  18. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-knowledge.page.ts +30 -0
  19. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-ontology.page.ts +71 -0
  20. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-profile.page.ts +38 -0
  21. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-teams.page.ts +123 -0
  22. package/template/.claude/skills/e2e-audit/e2e/pages/dashboard-transcripts.page.ts +109 -0
  23. package/template/.claude/skills/e2e-audit/e2e/specs/auth/login.spec.ts +59 -0
  24. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-admin.spec.ts +233 -0
  25. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-billing.spec.ts +44 -0
  26. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-chat.spec.ts +50 -0
  27. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-home.spec.ts +243 -0
  28. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-integrations.spec.ts +472 -0
  29. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-knowledge.spec.ts +57 -0
  30. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-ontology.spec.ts +72 -0
  31. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-profile.spec.ts +48 -0
  32. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-teams.spec.ts +247 -0
  33. package/template/.claude/skills/e2e-audit/e2e/specs/dashboard-transcripts.spec.ts +122 -0
  34. package/template/.claude/skills/e2e-audit/e2e/specs/security/headers.spec.ts +39 -0
  35. package/template/.claude/skills/e2e-audit/e2e/specs/security/rbac.spec.ts +92 -0
  36. package/template/.claude/skills/e2e-audit/e2e/specs/security/xss.spec.ts +74 -0
  37. package/template/.claude/skills/e2e-audit/e2e/utils/console-collector.ts +89 -0
  38. package/template/.claude/skills/e2e-audit/e2e/utils/security-helpers.ts +114 -0
  39. package/template/.claude/skills/e2e-audit/e2e/utils/test-data.ts +64 -0
  40. package/template/.claude/skills/e2e-audit/runbook.md +115 -0
  41. package/template/.claude/skills/super-design/SKILL.md +42 -4
  42. package/template/.claude/skills/super-design/references/audit-methodology.md +63 -7
  43. package/template/.claude/skills/super-design/scripts/discover-surfaces.sh +197 -0
  44. package/template/.claude/skills/super-design/scripts/extract-project-rules.sh +240 -0
  45. package/template/.claude/skills/super-design/scripts/verify-audit.sh +34 -1
@@ -0,0 +1,70 @@
1
+ import fs from 'fs'
2
+ import { test as setup } from '@playwright/test'
3
+ import { STORAGE_STATE } from './auth'
4
+
5
+ /**
6
+ * Auth setup — captures storage state for each role.
7
+ *
8
+ * If a storage state file already contains a session-token cookie,
9
+ * the setup SKIPS that role to avoid overwriting a valid session.
10
+ *
11
+ * To refresh sessions:
12
+ * 1. Delete the storage files in tests/e2e/fixtures/storage/
13
+ * 2. Log in manually at http://localhost:3000
14
+ * 3. Run `bunx playwright test --project=setup`
15
+ */
16
+
17
+ function hasValidSession(filePath: string): boolean {
18
+ try {
19
+ const data = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
20
+ return data.cookies?.some(
21
+ (c: { name: string }) => c.name === 'session-token'
22
+ )
23
+ } catch {
24
+ return false
25
+ }
26
+ }
27
+
28
+ setup('authenticate as owner', async ({ page }) => {
29
+ if (hasValidSession(STORAGE_STATE.owner)) {
30
+ // eslint-disable-next-line no-console
31
+ console.log('Owner session already valid, skipping setup')
32
+ return
33
+ }
34
+ await page.goto('/dashboard/home')
35
+ await page.waitForURL(/dashboard/, { timeout: 15000 }).catch(() => {})
36
+ await page.context().storageState({ path: STORAGE_STATE.owner })
37
+ })
38
+
39
+ setup('authenticate as admin', async ({ page }) => {
40
+ if (hasValidSession(STORAGE_STATE.admin)) {
41
+ // eslint-disable-next-line no-console
42
+ console.log('Admin session already valid, skipping setup')
43
+ return
44
+ }
45
+ await page.goto('/dashboard/home')
46
+ await page.waitForURL(/dashboard/, { timeout: 15000 }).catch(() => {})
47
+ await page.context().storageState({ path: STORAGE_STATE.admin })
48
+ })
49
+
50
+ setup('authenticate as manager', async ({ page }) => {
51
+ if (hasValidSession(STORAGE_STATE.manager)) {
52
+ // eslint-disable-next-line no-console
53
+ console.log('Manager session already valid, skipping setup')
54
+ return
55
+ }
56
+ await page.goto('/dashboard/home')
57
+ await page.waitForURL(/dashboard/, { timeout: 15000 }).catch(() => {})
58
+ await page.context().storageState({ path: STORAGE_STATE.manager })
59
+ })
60
+
61
+ setup('authenticate as member', async ({ page }) => {
62
+ if (hasValidSession(STORAGE_STATE.member)) {
63
+ // eslint-disable-next-line no-console
64
+ console.log('Member session already valid, skipping setup')
65
+ return
66
+ }
67
+ await page.goto('/dashboard/home')
68
+ await page.waitForURL(/dashboard/, { timeout: 15000 }).catch(() => {})
69
+ await page.context().storageState({ path: STORAGE_STATE.member })
70
+ })
@@ -0,0 +1,21 @@
1
+ import path from 'path'
2
+ import { fileURLToPath } from 'url'
3
+
4
+ const __filename = fileURLToPath(import.meta.url)
5
+ const __dirname = path.dirname(__filename)
6
+ const STORAGE_DIR = path.join(__dirname, 'storage')
7
+
8
+ /**
9
+ * Auth storage state paths per role.
10
+ *
11
+ * The actual auth setup tests live in auth.setup.ts.
12
+ * This file only exports the storage state paths and role type.
13
+ */
14
+ export const STORAGE_STATE = {
15
+ owner: path.join(STORAGE_DIR, 'owner.json'),
16
+ admin: path.join(STORAGE_DIR, 'admin.json'),
17
+ manager: path.join(STORAGE_DIR, 'manager.json'),
18
+ member: path.join(STORAGE_DIR, 'member.json'),
19
+ }
20
+
21
+ export type UserRole = keyof typeof STORAGE_STATE
@@ -0,0 +1,90 @@
1
+ import { test as base, expect, type Page } from '@playwright/test'
2
+ import { STORAGE_STATE, type UserRole } from './auth'
3
+
4
+ type ApiError = {
5
+ url: string
6
+ status: number
7
+ method: string
8
+ statusText: string
9
+ }
10
+
11
+ type Fixtures = {
12
+ authenticatedPage: Page
13
+ role: UserRole
14
+ apiErrors: ApiError[]
15
+ }
16
+
17
+ /**
18
+ * Endpoints that return errors due to missing env vars (e.g., Stripe),
19
+ * not actual application bugs. These are excluded from automatic failure reporting.
20
+ */
21
+ const IGNORED_API_PATTERNS = [
22
+ /\/api\/billing\//,
23
+ /ontology\.getGraph/,
24
+ /ontology\.getEntityTypes/,
25
+ ]
26
+
27
+ /**
28
+ * Extended test fixture with pre-authenticated page and API error tracking.
29
+ *
30
+ * Usage:
31
+ * import { test, expect } from '../fixtures/base'
32
+ * test('my test', async ({ authenticatedPage }) => { ... })
33
+ *
34
+ * Default role is 'owner'. Override per-describe:
35
+ * test.use({ role: 'member' })
36
+ *
37
+ * API errors (4xx, 5xx) are automatically tracked and reported in test failures.
38
+ * Env-dependent endpoints (billing, etc.) are excluded — see IGNORED_API_PATTERNS.
39
+ * Access via `apiErrors` fixture for explicit assertions.
40
+ */
41
+ export const test = base.extend<Fixtures>({
42
+ role: ['owner', { option: true }],
43
+
44
+ authenticatedPage: async ({ browser, role }, use) => {
45
+ const context = await browser.newContext({
46
+ storageState: STORAGE_STATE[role],
47
+ viewport: { width: 1280, height: 720 },
48
+ })
49
+ const page = await context.newPage()
50
+ await use(page)
51
+ await context.close()
52
+ },
53
+
54
+ apiErrors: async ({ authenticatedPage }, use) => {
55
+ const errors: ApiError[] = []
56
+
57
+ authenticatedPage.on('response', (response) => {
58
+ const status = response.status()
59
+ if (status >= 400) {
60
+ const url = response.url()
61
+ if (url.includes('/api/') || url.includes('/v1/')) {
62
+ const path = url.replace(/^https?:\/\/[^/]+/, '')
63
+ const isIgnored = IGNORED_API_PATTERNS.some((p) => p.test(path))
64
+ if (!isIgnored) {
65
+ errors.push({
66
+ url: path,
67
+ status,
68
+ method: response.request().method(),
69
+ statusText: response.statusText(),
70
+ })
71
+ }
72
+ }
73
+ }
74
+ })
75
+
76
+ await use(errors)
77
+
78
+ if (errors.length > 0) {
79
+ const report = errors
80
+ .map((e) => ` ${e.method} ${e.url} → ${e.status} ${e.statusText}`)
81
+ .join('\n')
82
+ expect(
83
+ errors,
84
+ `API errors detected during test:\n${report}`,
85
+ ).toHaveLength(0)
86
+ }
87
+ },
88
+ })
89
+
90
+ export { expect }
@@ -0,0 +1,50 @@
1
+ {
2
+ "cookies": [
3
+ {
4
+ "name": "session-token",
5
+ "value": "594120f0-6282-43d8-ba54-aeb6f65f9098",
6
+ "domain": "localhost",
7
+ "path": "/",
8
+ "expires": 1776952153.911341,
9
+ "httpOnly": true,
10
+ "secure": false,
11
+ "sameSite": "Lax"
12
+ },
13
+ {
14
+ "name": "csrf-token",
15
+ "value": "398b6e02b0b7171f76c0733b02ebf0dbd1c92b52c544d53cfc644b2ef3acbc90%7C2ea44d71177afdfd46dc06f295164ae67743b12c8164a058f786ae3fdf12d330",
16
+ "domain": "localhost",
17
+ "path": "/",
18
+ "expires": 1776918372.994304,
19
+ "httpOnly": true,
20
+ "secure": false,
21
+ "sameSite": "Lax"
22
+ },
23
+ {
24
+ "name": "NEXT_LOCALE",
25
+ "value": "pt-BR",
26
+ "domain": "localhost",
27
+ "path": "/",
28
+ "expires": 1807043134.189633,
29
+ "httpOnly": false,
30
+ "secure": false,
31
+ "sameSite": "Lax"
32
+ },
33
+ {
34
+ "name": "callback-url",
35
+ "value": "http%3A%2F%2Flocalhost%3A3000",
36
+ "domain": "localhost",
37
+ "path": "/",
38
+ "expires": 1776348335.004668,
39
+ "httpOnly": true,
40
+ "secure": false,
41
+ "sameSite": "Lax"
42
+ }
43
+ ],
44
+ "origins": [
45
+ {
46
+ "origin": "http://localhost:3000",
47
+ "localStorage": []
48
+ }
49
+ ]
50
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "cookies": [
3
+ {
4
+ "name": "session-token",
5
+ "value": "594120f0-6282-43d8-ba54-aeb6f65f9098",
6
+ "domain": "localhost",
7
+ "path": "/",
8
+ "expires": 1776952153.911341,
9
+ "httpOnly": true,
10
+ "secure": false,
11
+ "sameSite": "Lax"
12
+ },
13
+ {
14
+ "name": "csrf-token",
15
+ "value": "398b6e02b0b7171f76c0733b02ebf0dbd1c92b52c544d53cfc644b2ef3acbc90%7C2ea44d71177afdfd46dc06f295164ae67743b12c8164a058f786ae3fdf12d330",
16
+ "domain": "localhost",
17
+ "path": "/",
18
+ "expires": 1776918372.994304,
19
+ "httpOnly": true,
20
+ "secure": false,
21
+ "sameSite": "Lax"
22
+ },
23
+ {
24
+ "name": "NEXT_LOCALE",
25
+ "value": "pt-BR",
26
+ "domain": "localhost",
27
+ "path": "/",
28
+ "expires": 1807043134.189633,
29
+ "httpOnly": false,
30
+ "secure": false,
31
+ "sameSite": "Lax"
32
+ },
33
+ {
34
+ "name": "callback-url",
35
+ "value": "http%3A%2F%2Flocalhost%3A3000",
36
+ "domain": "localhost",
37
+ "path": "/",
38
+ "expires": 1776348335.004668,
39
+ "httpOnly": true,
40
+ "secure": false,
41
+ "sameSite": "Lax"
42
+ }
43
+ ],
44
+ "origins": [
45
+ {
46
+ "origin": "http://localhost:3000",
47
+ "localStorage": []
48
+ }
49
+ ]
50
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "cookies": [
3
+ {
4
+ "name": "session-token",
5
+ "value": "594120f0-6282-43d8-ba54-aeb6f65f9098",
6
+ "domain": "localhost",
7
+ "path": "/",
8
+ "expires": 1776952153.911341,
9
+ "httpOnly": true,
10
+ "secure": false,
11
+ "sameSite": "Lax"
12
+ },
13
+ {
14
+ "name": "csrf-token",
15
+ "value": "398b6e02b0b7171f76c0733b02ebf0dbd1c92b52c544d53cfc644b2ef3acbc90%7C2ea44d71177afdfd46dc06f295164ae67743b12c8164a058f786ae3fdf12d330",
16
+ "domain": "localhost",
17
+ "path": "/",
18
+ "expires": 1776918372.994304,
19
+ "httpOnly": true,
20
+ "secure": false,
21
+ "sameSite": "Lax"
22
+ },
23
+ {
24
+ "name": "NEXT_LOCALE",
25
+ "value": "pt-BR",
26
+ "domain": "localhost",
27
+ "path": "/",
28
+ "expires": 1807043134.189633,
29
+ "httpOnly": false,
30
+ "secure": false,
31
+ "sameSite": "Lax"
32
+ },
33
+ {
34
+ "name": "callback-url",
35
+ "value": "http%3A%2F%2Flocalhost%3A3000",
36
+ "domain": "localhost",
37
+ "path": "/",
38
+ "expires": 1776348335.004668,
39
+ "httpOnly": true,
40
+ "secure": false,
41
+ "sameSite": "Lax"
42
+ }
43
+ ],
44
+ "origins": [
45
+ {
46
+ "origin": "http://localhost:3000",
47
+ "localStorage": []
48
+ }
49
+ ]
50
+ }
@@ -0,0 +1,50 @@
1
+ {
2
+ "cookies": [
3
+ {
4
+ "name": "session-token",
5
+ "value": "594120f0-6282-43d8-ba54-aeb6f65f9098",
6
+ "domain": "localhost",
7
+ "path": "/",
8
+ "expires": 1776952153.911341,
9
+ "httpOnly": true,
10
+ "secure": false,
11
+ "sameSite": "Lax"
12
+ },
13
+ {
14
+ "name": "csrf-token",
15
+ "value": "398b6e02b0b7171f76c0733b02ebf0dbd1c92b52c544d53cfc644b2ef3acbc90%7C2ea44d71177afdfd46dc06f295164ae67743b12c8164a058f786ae3fdf12d330",
16
+ "domain": "localhost",
17
+ "path": "/",
18
+ "expires": 1776918372.994304,
19
+ "httpOnly": true,
20
+ "secure": false,
21
+ "sameSite": "Lax"
22
+ },
23
+ {
24
+ "name": "NEXT_LOCALE",
25
+ "value": "pt-BR",
26
+ "domain": "localhost",
27
+ "path": "/",
28
+ "expires": 1807043134.189633,
29
+ "httpOnly": false,
30
+ "secure": false,
31
+ "sameSite": "Lax"
32
+ },
33
+ {
34
+ "name": "callback-url",
35
+ "value": "http%3A%2F%2Flocalhost%3A3000",
36
+ "domain": "localhost",
37
+ "path": "/",
38
+ "expires": 1776348335.004668,
39
+ "httpOnly": true,
40
+ "secure": false,
41
+ "sameSite": "Lax"
42
+ }
43
+ ],
44
+ "origins": [
45
+ {
46
+ "origin": "http://localhost:3000",
47
+ "localStorage": []
48
+ }
49
+ ]
50
+ }
@@ -0,0 +1,141 @@
1
+ import type { Page, Locator } from '@playwright/test'
2
+
3
+ export class DashboardAdminPage {
4
+ readonly page: Page
5
+
6
+ // Page header
7
+ readonly heading: Locator
8
+ readonly adminBadge: Locator
9
+ readonly welcomeText: Locator
10
+
11
+ // Tabs
12
+ readonly organizationsTab: Locator
13
+ readonly usersTab: Locator
14
+ readonly systemOverviewTab: Locator
15
+
16
+ // --- Organizations Tab ---
17
+ readonly orgHeading: Locator
18
+ readonly orgRefreshButton: Locator
19
+ readonly orgSearchInput: Locator
20
+ readonly orgTable: Locator
21
+ readonly newOrgButton: Locator
22
+
23
+ // --- Users Tab ---
24
+ readonly usersHeading: Locator
25
+ readonly usersRefreshButton: Locator
26
+ readonly usersSearchInput: Locator
27
+ readonly usersTable: Locator
28
+
29
+ // --- System Overview Tab ---
30
+ readonly systemHeading: Locator
31
+ readonly systemErrorHeading: Locator
32
+ readonly systemRetryButton: Locator
33
+
34
+ // --- Organization Detail Dialog ---
35
+ readonly orgDetailDialog: Locator
36
+ readonly orgDetailOverviewTab: Locator
37
+ readonly orgDetailMembersTab: Locator
38
+ readonly orgDetailSettingsTab: Locator
39
+ readonly orgDetailEditButton: Locator
40
+ readonly orgDetailDeleteButton: Locator
41
+
42
+ // --- New Organization Dialog ---
43
+ readonly newOrgNameInput: Locator
44
+ readonly newOrgSubdomainInput: Locator
45
+ readonly newOrgPrevButton: Locator
46
+ readonly newOrgNextButton: Locator
47
+
48
+ // --- User Edit Dialog ---
49
+ readonly userEditNameInput: Locator
50
+ readonly userEditStatusCombobox: Locator
51
+ readonly userEditSaveButton: Locator
52
+
53
+ constructor(page: Page) {
54
+ this.page = page
55
+
56
+ // Page header
57
+ this.heading = page.getByRole('heading', { level: 1 })
58
+ this.adminBadge = page.getByText('Administrador').first()
59
+ this.welcomeText = page.getByText(/Bem-vindo/i)
60
+
61
+ // Tabs
62
+ this.organizationsTab = page.getByRole('tab', { name: /Organiza/i })
63
+ this.usersTab = page.getByRole('tab', { name: /Usu/i })
64
+ this.systemOverviewTab = page.getByRole('tab', { name: /Vis.*Sistema/i })
65
+
66
+ // --- Organizations Tab ---
67
+ this.orgHeading = page.getByRole('heading', { name: /Gerenciamento de Organiza/i })
68
+ this.orgRefreshButton = page.getByRole('button', { name: /Atualizar/i })
69
+ this.orgSearchInput = page.getByRole('textbox', { name: /buscar.*nome.*subdom/i })
70
+ this.orgTable = page.getByRole('table')
71
+ this.newOrgButton = page.getByRole('button', { name: /Nova Organiza/i })
72
+
73
+ // --- Users Tab ---
74
+ this.usersHeading = page.getByRole('heading', { name: /Gerenciamento de Usu/i })
75
+ this.usersRefreshButton = page.getByRole('button', { name: /Atualizar/i })
76
+ this.usersSearchInput = page.getByRole('textbox', { name: /buscar.*nome.*e-mail/i })
77
+ this.usersTable = page.getByRole('table')
78
+
79
+ // --- System Overview Tab ---
80
+ this.systemHeading = page.getByRole('heading', { name: /Vis.*Geral.*Sistema/i, level: 2 })
81
+ this.systemErrorHeading = page.getByRole('heading', { name: /Falha ao carregar/i })
82
+ this.systemRetryButton = page.getByRole('button', { name: /Tentar Novamente/i })
83
+
84
+ // --- Organization Detail Dialog ---
85
+ this.orgDetailDialog = page.getByRole('dialog')
86
+ this.orgDetailOverviewTab = page.getByRole('dialog').getByRole('tab', { name: /Vis.*Geral/i })
87
+ this.orgDetailMembersTab = page.getByRole('dialog').getByRole('tab', { name: /Membros/i })
88
+ this.orgDetailSettingsTab = page.getByRole('dialog').getByRole('tab', { name: /Configura/i })
89
+ this.orgDetailEditButton = page.getByRole('dialog').getByRole('button', { name: /Editar Organiza/i })
90
+ this.orgDetailDeleteButton = page.getByRole('dialog').getByRole('button', { name: /Excluir Organiza/i })
91
+
92
+ // --- New Organization Dialog ---
93
+ this.newOrgNameInput = page.getByRole('dialog').getByRole('textbox', { name: /Nome da Organiza/i })
94
+ this.newOrgSubdomainInput = page.getByRole('dialog').getByRole('textbox', { name: /Subdom/i })
95
+ this.newOrgPrevButton = page.getByRole('dialog').getByRole('button', { name: /Anterior/i })
96
+ this.newOrgNextButton = page.getByRole('dialog').getByRole('button', { name: /Próximo/i })
97
+
98
+ // --- User Edit Dialog ---
99
+ this.userEditNameInput = page.getByRole('dialog').getByRole('textbox', { name: /Nome/i })
100
+ this.userEditStatusCombobox = page.getByRole('dialog').getByRole('combobox').first()
101
+ this.userEditSaveButton = page.getByRole('dialog').getByRole('button', { name: /Salvar/i })
102
+ }
103
+
104
+ async goto(tab?: 'organizations' | 'users' | 'system') {
105
+ const url = tab ? `/dashboard/admin?tab=${tab}` : '/dashboard/admin'
106
+ await this.page.goto(url)
107
+ }
108
+
109
+ async waitForLoad() {
110
+ await this.heading.waitFor({ timeout: 15000 })
111
+ }
112
+
113
+ async switchTab(tab: 'organizations' | 'users' | 'system') {
114
+ const tabLocator = {
115
+ organizations: this.organizationsTab,
116
+ users: this.usersTab,
117
+ system: this.systemOverviewTab,
118
+ }[tab]
119
+ await tabLocator.click()
120
+ }
121
+
122
+ async clickOrgRow(orgName: string) {
123
+ await this.page.getByRole('row').filter({ hasText: orgName }).click()
124
+ }
125
+
126
+ async clickUserRow(userName: string) {
127
+ await this.page.getByRole('row').filter({ hasText: userName }).click()
128
+ }
129
+
130
+ async getOrgRowCount() {
131
+ return this.orgTable.getByRole('row').filter({ hasText: /Linha \d+/ }).count()
132
+ }
133
+
134
+ async getUserRowCount() {
135
+ return this.usersTable.getByRole('row').filter({ hasText: /Linha \d+/ }).count()
136
+ }
137
+
138
+ async closeDialog() {
139
+ await this.page.getByRole('dialog').getByRole('button', { name: /Close/i }).click()
140
+ }
141
+ }
@@ -0,0 +1,47 @@
1
+ import type { Page, Locator } from '@playwright/test'
2
+
3
+ export class DashboardBillingPage {
4
+ readonly page: Page
5
+
6
+ // Page header
7
+ readonly heading: Locator
8
+ readonly description: Locator
9
+
10
+ // Subscription card
11
+ readonly planLogo: Locator
12
+ readonly planName: Locator
13
+ readonly planLabel: Locator
14
+ readonly activeUsersLabel: Locator
15
+ readonly statusLabel: Locator
16
+ readonly subscribeButton: Locator
17
+
18
+ // Footer
19
+ readonly stripeSecurityText: Locator
20
+
21
+ constructor(page: Page) {
22
+ this.page = page
23
+
24
+ // Page header
25
+ this.heading = page.getByRole('heading', { level: 1 })
26
+ this.description = page.getByText(/Gerencie sua assinatura/i)
27
+
28
+ // Subscription card
29
+ this.planLogo = page.locator('main img').first()
30
+ this.planName = page.getByText('Hakutaku Standard').first()
31
+ this.planLabel = page.getByText(/Plano/i).first()
32
+ this.activeUsersLabel = page.getByText(/Usuários ativos/i)
33
+ this.statusLabel = page.getByText(/Status/i).first()
34
+ this.subscribeButton = page.getByRole('button', { name: /Assinar agora/i })
35
+
36
+ // Footer
37
+ this.stripeSecurityText = page.getByText(/Pagamentos processados com segurança/i)
38
+ }
39
+
40
+ async goto() {
41
+ await this.page.goto('/dashboard/billing')
42
+ }
43
+
44
+ async waitForLoad() {
45
+ await this.heading.waitFor({ timeout: 15000 })
46
+ }
47
+ }
@@ -0,0 +1,35 @@
1
+ import type { Page, Locator } from '@playwright/test'
2
+
3
+ export class DashboardChatPage {
4
+ readonly page: Page
5
+
6
+ readonly heading: Locator
7
+ readonly messageInput: Locator
8
+ readonly sendButton: Locator
9
+ readonly messageList: Locator
10
+ readonly newChatButton: Locator
11
+ readonly sessionList: Locator
12
+
13
+ constructor(page: Page) {
14
+ this.page = page
15
+ this.heading = page.getByRole('heading').first()
16
+ this.messageInput = page.getByRole('textbox').first()
17
+ this.sendButton = page.getByRole('button', { name: /send/i })
18
+ this.messageList = page.locator('[data-testid="message-list"]')
19
+ this.newChatButton = page.getByRole('button', { name: /new.*chat/i })
20
+ this.sessionList = page.locator('[data-testid="session-list"]')
21
+ }
22
+
23
+ async goto() {
24
+ await this.page.goto('/dashboard/chat')
25
+ }
26
+
27
+ async waitForLoad() {
28
+ await this.page.waitForLoadState('networkidle')
29
+ }
30
+
31
+ async sendMessage(text: string) {
32
+ await this.messageInput.fill(text)
33
+ await this.sendButton.click()
34
+ }
35
+ }