Skip to content

Commit 5123adf

Browse files
committed
E2E improvements
1 parent 1677e4a commit 5123adf

10 files changed

Lines changed: 389 additions & 343 deletions

File tree

e2e/dogfooding.spec.ts

Lines changed: 160 additions & 245 deletions
Large diffs are not rendered by default.

e2e/helpers/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export { login } from './login.js'
2+
export { expectNotification } from './notification.js'
3+
export { addPrerequisite, type PrerequisiteParams } from './prerequisite.js'
4+
export { addRepository, type AddRepositoryParams } from './repository.js'
5+
export { fillServiceForm, navigateToCreateService, submitServiceForm, type CreateServiceParams } from './service.js'
6+
export { navigateViaSidebar } from './sidebar.js'
7+
export { createStack, deleteStack, type CreateStackParams } from './stack.js'
File renamed without changes.

e2e/helpers/notification.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { Page } from '@playwright/test'
2+
import { expect } from '@playwright/test'
3+
4+
export const expectNotification = async (page: Page, text: string) => {
5+
await expect(page.locator('shade-noty-list')).toContainText(text)
6+
}

e2e/helpers/prerequisite.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { Page } from '@playwright/test'
2+
import { expect } from '@playwright/test'
3+
4+
import { expectNotification } from './notification.js'
5+
6+
export type PrerequisiteParams = {
7+
name: string
8+
type: 'Node.js' | 'Yarn' | 'Git' | 'Environment Variable'
9+
minimumVersion?: string
10+
variableName?: string
11+
isSensitive?: boolean
12+
}
13+
14+
export const addPrerequisite = async (page: Page, params: PrerequisiteParams) => {
15+
const prereqForm = page.locator('shade-prerequisite-form')
16+
const typeSelect = page.locator('shade-select').filter({ has: page.locator('input[name="type"]') })
17+
18+
await page.locator('button', { hasText: 'Add Prerequisite' }).click()
19+
await expect(prereqForm).toBeVisible()
20+
await prereqForm.locator('input[name="name"]').fill(params.name)
21+
await typeSelect.locator('.select-trigger').click()
22+
await typeSelect.locator('.dropdown-item', { hasText: params.type }).first().click()
23+
24+
if (params.minimumVersion) {
25+
await prereqForm.locator('input[name="minimumVersion"]').fill(params.minimumVersion)
26+
}
27+
if (params.variableName) {
28+
await prereqForm.locator('input[name="variableName"]').fill(params.variableName)
29+
}
30+
if (params.isSensitive) {
31+
await prereqForm.locator('input[name="isSensitive"]').check()
32+
}
33+
34+
await prereqForm.locator('button', { hasText: 'Add' }).click()
35+
await expectNotification(page, `"${params.name}" was added.`)
36+
}

e2e/helpers/repository.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import type { Page } from '@playwright/test'
2+
import { expect } from '@playwright/test'
3+
4+
import { navigateViaSidebar } from './sidebar.js'
5+
6+
export type AddRepositoryParams = {
7+
displayName: string
8+
url: string
9+
}
10+
11+
export const addRepository = async (page: Page, stackDisplayName: string, params: AddRepositoryParams) => {
12+
await navigateViaSidebar(page, stackDisplayName, 'Repositories')
13+
await expect(page.locator('shade-repositories-list')).toBeVisible()
14+
15+
await page.locator('button', { hasText: 'Add Repository' }).first().click()
16+
await expect(page.locator('shade-create-repository')).toBeVisible()
17+
18+
await page.locator('input[name="displayName"]').fill(params.displayName)
19+
await page.locator('input[name="url"]').fill(params.url)
20+
await page.locator('button', { hasText: 'Add' }).click()
21+
22+
await expect(page.locator('shade-repositories-list')).toBeVisible()
23+
}

e2e/helpers/service.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { Page } from '@playwright/test'
2+
import { expect } from '@playwright/test'
3+
4+
import { navigateViaSidebar } from './sidebar.js'
5+
6+
export type CreateServiceParams = {
7+
displayName: string
8+
workingDirectory: string
9+
runCommand: string
10+
installCommand?: string
11+
buildCommand?: string
12+
}
13+
14+
export const navigateToCreateService = async (page: Page, stackDisplayName: string) => {
15+
await navigateViaSidebar(page, stackDisplayName, 'Services')
16+
await expect(page.locator('shade-services-list')).toBeVisible()
17+
await page.locator('button', { hasText: 'Create Service' }).first().click()
18+
await expect(page.locator('shade-create-service-wizard')).toBeVisible()
19+
}
20+
21+
export const fillServiceForm = async (page: Page, params: CreateServiceParams) => {
22+
await page.locator('input[name="displayName"]').fill(params.displayName)
23+
await page.locator('input[name="workingDirectory"]').fill(params.workingDirectory)
24+
await page.locator('input[name="runCommand"]').fill(params.runCommand)
25+
if (params.installCommand) {
26+
await page.locator('input[name="installCommand"]').fill(params.installCommand)
27+
}
28+
if (params.buildCommand) {
29+
await page.locator('input[name="buildCommand"]').fill(params.buildCommand)
30+
}
31+
}
32+
33+
export const submitServiceForm = async (page: Page) => {
34+
await page.locator('button', { hasText: 'Create' }).click()
35+
}

e2e/helpers/sidebar.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import type { Page } from '@playwright/test'
2+
3+
export const navigateViaSidebar = async (
4+
page: Page,
5+
stackDisplayName: string,
6+
link: 'Overview' | 'Services' | 'Repositories' | 'Prerequisites',
7+
) => {
8+
const stackSidebar = page.locator('shade-accordion-item').filter({ hasText: stackDisplayName })
9+
10+
// Expand the accordion if it's collapsed
11+
const isExpanded = await stackSidebar.getAttribute('data-expanded')
12+
if (isExpanded === null) {
13+
await stackSidebar.locator('.accordion-header').click()
14+
}
15+
16+
await stackSidebar.locator('shade-sidebar-stack-link a', { hasText: link }).click()
17+
}

e2e/helpers/stack.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { Page } from '@playwright/test'
2+
import { expect } from '@playwright/test'
3+
4+
import { expectNotification } from './notification.js'
5+
6+
export type CreateStackParams = {
7+
name: string
8+
displayName: string
9+
description: string
10+
mainDirectory: string
11+
}
12+
13+
export const createStack = async (page: Page, params: CreateStackParams) => {
14+
await page.locator('button, a', { hasText: 'Create Stack' }).first().click()
15+
await expect(page.locator('shade-create-stack')).toBeVisible()
16+
17+
await page.locator('input[name="name"]').fill(params.name)
18+
await page.locator('input[name="displayName"]').fill(params.displayName)
19+
await page.locator('textarea[name="description"]').fill(params.description)
20+
await page.locator('input[name="mainDirectory"]').fill(params.mainDirectory)
21+
await page.locator('button', { hasText: 'Create' }).click()
22+
23+
await expectNotification(page, `Stack "${params.displayName}" was created successfully.`)
24+
await expect(page.locator('shade-dashboard')).toBeVisible()
25+
await expect(page.getByTestId('page-header-title')).toContainText(params.displayName)
26+
}
27+
28+
export const deleteStack = async (page: Page, displayName: string) => {
29+
// Navigate to the main dashboard via the sidebar "Dashboard" link (always visible, no accordion)
30+
await page.locator('shade-sidebar-item a', { hasText: 'Dashboard' }).click()
31+
await expect(page.locator('stack-list-dashboard')).toBeVisible()
32+
33+
// Click on the stack card to open its overview
34+
await page.locator('stack-list-dashboard shade-card', { hasText: displayName }).click()
35+
await expect(page.getByTestId('page-header-title')).toContainText(displayName)
36+
37+
// Navigate to Edit Stack, then delete
38+
await page.locator('a', { hasText: 'Edit Stack' }).click()
39+
await expect(page.locator('shade-edit-stack')).toBeVisible()
40+
41+
await page.locator('button', { hasText: 'Delete Stack' }).click()
42+
await page.locator('shade-dialog .dialog-confirm-btn').click()
43+
44+
await expectNotification(page, `"${displayName}" was deleted.`)
45+
await expect(page.locator('shade-dashboard')).toBeVisible({ timeout: 10_000 })
46+
}

e2e/smoke.spec.ts

Lines changed: 59 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,62 @@
11
import { expect, test } from '@playwright/test'
2-
import { login } from './helpers.js'
32

4-
test.describe.serial('App Flow', () => {
5-
let stackName: string
6-
let displayName: string
7-
8-
test('Login and view dashboard', async ({ page }) => {
9-
await page.goto('/')
10-
await login(page)
11-
})
12-
13-
test('Create stack, create service, verify dashboard', async ({ page, browserName }) => {
14-
const uuid = crypto.randomUUID()
15-
16-
stackName = `e2e-test-stack-${uuid}`
17-
displayName = `E2E Test Stack - ${browserName} - ${uuid}`
18-
19-
const workingDirectory = `/tmp/e2e-test-stack-${uuid}`
20-
21-
await page.goto('/')
22-
await login(page)
23-
24-
// Create stack
25-
await page.locator('button, a', { hasText: 'Create Stack' }).first().click()
26-
await expect(page.locator('shade-create-stack')).toBeVisible()
27-
28-
await page.locator('input[name="name"]').fill(stackName)
29-
await page.locator('input[name="displayName"]').fill(displayName)
30-
await page.locator('textarea[name="description"]').fill('Created by E2E test')
31-
await page.locator('input[name="mainDirectory"]').fill('/tmp/e2e-test')
32-
await page.locator('button', { hasText: 'Create' }).click()
33-
34-
await expect(page.locator('shade-noty-list')).toContainText(`Stack "${displayName}" was created successfully.`)
35-
36-
await expect(page.locator('shade-dashboard')).toBeVisible()
37-
38-
await expect(page.getByTestId('page-header-title')).toContainText(displayName)
39-
40-
// Navigate to services list via the dashboard card's "View All" link
41-
await page.locator('shade-dashboard a', { hasText: 'View All' }).first().click()
42-
await expect(page.locator('shade-services-list')).toBeVisible()
43-
await page.locator('button', { hasText: 'Create Service' }).first().click()
44-
await expect(page.locator('shade-create-service-wizard')).toBeVisible()
45-
46-
await page.locator('input[name="displayName"]').fill('E2E Service')
47-
await page.locator('input[name="workingDirectory"]').fill(workingDirectory)
48-
await page.locator('input[name="runCommand"]').fill('echo hello')
49-
await page.locator('button', { hasText: 'Create' }).click()
50-
51-
await expect(page.locator('shade-services-list')).toBeVisible()
52-
53-
// Scope all sidebar interactions to the correct stack
54-
const stackSidebar = page.locator('shade-accordion-item').filter({ hasText: displayName })
55-
56-
// Navigate to repositories list
57-
await stackSidebar.locator('shade-sidebar-stack-link a', { hasText: 'Repositories' }).click()
58-
await expect(page.locator('shade-repositories-list')).toBeVisible()
59-
await page.locator('button', { hasText: 'Add Repository' }).first().click()
60-
await expect(page.locator('shade-create-repository')).toBeVisible()
61-
62-
await page.locator('input[name="displayName"]').fill('FuryStack')
63-
await page.locator('input[name="url"]').fill('https://github.com/furystack/furystack')
64-
await page.locator('button', { hasText: 'Add' }).click()
65-
66-
await expect(page.locator('shade-repositories-list')).toBeVisible()
67-
68-
// Navigate to services list
69-
await stackSidebar.locator('shade-sidebar-stack-link a', { hasText: 'Services' }).click()
70-
await expect(page.locator('shade-services-list')).toBeVisible()
71-
await page.locator('button', { hasText: 'Create Service' }).first().click()
72-
await expect(page.locator('shade-create-service-wizard')).toBeVisible()
73-
74-
await page.locator('input[name="displayName"]').fill('StackCraft DOG FOODING TIME!')
75-
await page.locator('input[name="workingDirectory"]').fill(workingDirectory)
76-
await page.locator('input[name="runCommand"]').fill('echo hello')
77-
await page.locator('button', { hasText: 'Create' }).click()
78-
79-
await expect(page.locator('shade-services-list')).toBeVisible()
80-
})
81-
82-
test('Clean up stack', async ({ page }) => {
83-
await page.goto('/')
84-
await login(page)
85-
86-
// Click on the stack card in the main dashboard to open its overview
87-
await page.locator('stack-list-dashboard shade-card', { hasText: displayName }).click()
88-
await expect(page.getByTestId('page-header-title')).toContainText(displayName)
89-
90-
// Navigate to Edit Stack via the header button
91-
await page.locator('a', { hasText: 'Edit Stack' }).click()
92-
await expect(page.locator('shade-edit-stack')).toBeVisible()
93-
94-
// Delete the stack
95-
await page.locator('button', { hasText: 'Delete Stack' }).click()
96-
await page.locator('shade-dialog .dialog-confirm-btn').click()
97-
98-
await expect(page.locator('shade-noty-list')).toContainText(`"${displayName}" was deleted.`)
99-
await expect(page.locator('shade-dashboard')).toBeVisible({ timeout: 10000 })
100-
})
3+
import {
4+
addRepository,
5+
createStack,
6+
deleteStack,
7+
fillServiceForm,
8+
login,
9+
navigateToCreateService,
10+
submitServiceForm,
11+
} from './helpers/index.js'
12+
13+
test('App Flow', async ({ page, browserName }) => {
14+
const uuid = crypto.randomUUID()
15+
16+
const stackName = `e2e-test-stack-${uuid}`
17+
const displayName = `E2E Test Stack - ${browserName} - ${uuid}`
18+
const workingDirectory = `/tmp/e2e-test-stack-${uuid}`
19+
20+
await page.goto('/')
21+
await login(page)
22+
23+
try {
24+
await test.step('Create stack', async () => {
25+
await createStack(page, {
26+
name: stackName,
27+
displayName,
28+
description: 'Created by E2E test',
29+
mainDirectory: '/tmp/e2e-test',
30+
})
31+
})
32+
33+
await test.step('Create first service', async () => {
34+
await navigateToCreateService(page, displayName)
35+
await fillServiceForm(page, { displayName: 'E2E Service', workingDirectory, runCommand: 'echo hello' })
36+
await submitServiceForm(page)
37+
await expect(page.locator('shade-services-list')).toBeVisible()
38+
})
39+
40+
await test.step('Add repository', async () => {
41+
await addRepository(page, displayName, {
42+
displayName: 'FuryStack',
43+
url: 'https://github.com/furystack/furystack',
44+
})
45+
})
46+
47+
await test.step('Create second service', async () => {
48+
await navigateToCreateService(page, displayName)
49+
await fillServiceForm(page, {
50+
displayName: 'StackCraft DOG FOODING TIME!',
51+
workingDirectory,
52+
runCommand: 'echo hello',
53+
})
54+
await submitServiceForm(page)
55+
await expect(page.locator('shade-services-list')).toBeVisible()
56+
})
57+
} finally {
58+
await test.step('Clean up stack', async () => {
59+
await deleteStack(page, displayName)
60+
})
61+
}
10162
})

0 commit comments

Comments
 (0)