start-vibing 4.3.4 → 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/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,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 }
|
|
File without changes
|
|
@@ -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
|
+
}
|