Skip to content

Commit 0584db0

Browse files
committed
improved test coverage
1 parent 751a8bc commit 0584db0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+2954
-2537
lines changed

.cursor/rules/SHADES_COMPONENTS.mdc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,6 @@ Export pages from an index file:
374374
// frontend/src/pages/index.ts
375375
export * from './dashboard.js'
376376
export * from './login.js'
377-
export * from './hello-world.js'
378377
```
379378

380379
## Application Entry Point

.cursor/rules/TESTING_GUIDELINES.mdc

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,56 @@ describe('ObservableValue', () => {
191191
})
192192
```
193193

194+
## Backend Service Testing with Test Helpers
195+
196+
### Using `withTestInjector`
197+
198+
For testing backend services and REST actions, use the shared test helpers:
199+
200+
```typescript
201+
import { describe, it, expect, vi } from 'vitest'
202+
import { getRepository } from '@furystack/repository'
203+
import { ServiceDefinition, ServiceStatus } from 'common'
204+
205+
import { withTestInjector, createMockActionContext } from '../test-helpers.js'
206+
207+
describe('MyAction', () => {
208+
it('should return data for valid request', () =>
209+
withTestInjector(async ({ injector, elevated }) => {
210+
// Seed test data
211+
await getRepository(elevated).getDataSetFor(ServiceDefinition, 'id').add(elevated, {
212+
id: 'svc-1',
213+
stackName: 'test-stack',
214+
displayName: 'Test Service',
215+
// ... other required fields
216+
})
217+
218+
// Mock external services via setExplicitInstance
219+
const mockService = { someMethod: vi.fn().mockResolvedValue('result') }
220+
injector.setExplicitInstance(mockService as unknown as MyService, MyService)
221+
222+
// Create and execute action
223+
const ctx = createMockActionContext({
224+
injector: elevated,
225+
urlParams: { id: 'svc-1' },
226+
})
227+
const result = await myAction(ctx)
228+
229+
// Assert
230+
expect(result.chunk.status).toBe(200)
231+
}),
232+
)
233+
})
234+
```
235+
236+
### Key patterns
237+
238+
- `withTestInjector` provides an `injector` + `elevated` (system-level) context with all InMemoryStores pre-configured
239+
- `createMockActionContext` creates a typed request context for REST action testing
240+
- Use `getRepository(elevated).getDataSetFor(...)` to seed and verify data
241+
- Use `injector.setExplicitInstance(mock, ServiceClass)` for mocking DI services
242+
- Both injectors are automatically disposed after each test
243+
194244
## E2E Testing with Playwright
195245

196246
### Test File Location
@@ -278,9 +328,8 @@ test.describe('Authentication', () => {
278328
await page.locator('button', { hasText: 'Login' }).click()
279329

280330
// Verify logged in state
281-
const welcomeTitle = page.locator('hello-world div h2')
282-
await expect(welcomeTitle).toBeVisible()
283-
await expect(welcomeTitle).toHaveText('Hello, testuser !')
331+
const dashboard = page.locator('page-dashboard')
332+
await expect(dashboard).toBeVisible()
284333

285334
// Logout
286335
const logoutButton = page.locator('shade-app-bar button >> text="Log Out"')

common/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export * from './models/index.js'
22
export * from './apis/index.js'
33
export * from './utils/service-path-utils.js'
4+
export * from './utils/merge-service-view.js'
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import type { ServiceConfig } from '../models/service-config.js'
2+
import type { ServiceDefinition } from '../models/service-definition.js'
3+
import type { ServiceGitStatus } from '../models/service-git-status.js'
4+
import type { ServiceStatus } from '../models/service-status.js'
5+
import type { ServiceView } from '../models/views.js'
6+
7+
export const mergeServiceView = (
8+
def: ServiceDefinition,
9+
config?: ServiceConfig,
10+
status?: ServiceStatus,
11+
gitStatus?: ServiceGitStatus,
12+
): ServiceView => ({
13+
serviceId: def.id,
14+
autoFetchEnabled: false,
15+
autoFetchIntervalMinutes: 60,
16+
autoRestartOnFetch: false,
17+
environmentVariableOverrides: {},
18+
localFiles: [],
19+
cloneStatus: 'not-cloned',
20+
installStatus: 'not-installed',
21+
buildStatus: 'not-built',
22+
runStatus: 'stopped',
23+
...def,
24+
...(config ?? {}),
25+
...(status ?? {}),
26+
...(gitStatus ?? {}),
27+
})

e2e/dogfooding.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ test('DOG FOODING TIME - Create a service that uses the StackCraft GitHub reposi
88
const uuid = crypto.randomUUID()
99

1010
const stackName = `e2e-dog-fooding-time-${uuid}`
11-
const displayName = `E2E DOG FOODING Stack - ${browserName} - ${uuid}`
11+
const displayName = `Dog Fooding - ${browserName} - ${uuid}`
1212
const description = `
13-
### 🐶🦴 E2E - IT'S DOG FOODING TIME 🐶🦴
13+
##### 🐶🦴 E2E - IT'S DOG FOODING TIME 🐶🦴
1414
1515
This stack is used to test the dogfooding of the StackCraft application.
1616
It is used to test the following features:
@@ -21,7 +21,7 @@ It is used to test the following features:
2121
- Creating a service that uses the StackCraft GitHub repository
2222
- Cloning, installing, building and running the service
2323
24-
### Test Steps
24+
##### Test Steps
2525
2626
1. Create a stack
2727
2. Create a service with prerequisites, env vars, file overrides, and the StackCraft GitHub repository

frontend/public/style.css

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,24 @@ head {
33
margin: 0;
44
padding: 0;
55
font-family: 'Franklin Gothic Medium', 'Arial Narrow', Arial, sans-serif;
6+
scrollbar-width: thin;
7+
scrollbar-color: rgba(128, 128, 128, 0.25) transparent;
68
}
79

8-
/* width */
910
::-webkit-scrollbar {
10-
width: 10px;
11+
width: 6px;
12+
height: 6px;
1113
}
1214

13-
/* Track */
1415
::-webkit-scrollbar-track {
15-
background: rgba(128, 128, 128, 0.3);
16+
background: transparent;
1617
}
1718

18-
/* Handle */
1919
::-webkit-scrollbar-thumb {
20-
background: #888;
20+
background: rgba(128, 128, 128, 0.25);
21+
border-radius: 3px;
2122
}
2223

23-
/* Handle on hover */
2424
::-webkit-scrollbar-thumb:hover {
25-
background: #555;
25+
background: rgba(128, 128, 128, 0.5);
2626
}

frontend/src/components/app-routes.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import { PrerequisitesList } from '../pages/prerequisites/prerequisites-list.js'
77
import { CreateRepository } from '../pages/repositories/create-repository.js'
88
import { EditRepository } from '../pages/repositories/edit-repository.js'
99
import { RepositoriesList } from '../pages/repositories/repositories-list.js'
10-
import { CreateService } from '../pages/services/create-service.js'
11-
import { ServiceDetail } from '../pages/services/service-detail.js'
10+
import { ServiceDetail } from '../pages/services/service-detail/index.js'
1211
import { ServiceLogs } from '../pages/services/service-logs.js'
1312
import { ServicesList } from '../pages/services/services-list.js'
1413
import { UserSettings } from '../pages/settings/user-settings.js'
@@ -55,11 +54,6 @@ export const appRoutes = {
5554
<ServicesList stackName={match.params.stackName} />
5655
),
5756
},
58-
'/stacks/:stackName/services/create': {
59-
component: ({ match }: { match: MatchResult<{ stackName: string }> }) => (
60-
<CreateService stackName={match.params.stackName} />
61-
),
62-
},
6357
'/stacks/:stackName/services/wizard': {
6458
component: ({ match }: { match: MatchResult<{ stackName: string }> }) => (
6559
<CreateServiceWizard stackName={match.params.stackName} />
@@ -104,7 +98,7 @@ export const appRoutes = {
10498
<PrerequisitesList stackName={match.params.stackName} />
10599
),
106100
},
107-
} as const satisfies Record<string, NestedRoute<any>>
101+
} as const satisfies Record<string, NestedRoute<any>> // NestedRouterProps requires `any` for heterogeneous route params
108102

109103
export const StackCraftNestedRouteLink = createNestedRouteLink<typeof appRoutes>()
110104

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { Injector } from '@furystack/inject'
22
import { createComponent, NestedRouter, Shade } from '@furystack/shades'
3-
import { Init, Offline } from '../pages/index.js'
4-
import { NotFound } from '../pages/not-found.js'
5-
import { SessionService } from '../services/session.js'
6-
import { appRoutes } from './app-routes.js'
3+
import { Init, Offline } from '../../pages/index.js'
4+
import { NotFound } from '../../pages/not-found.js'
5+
import { SessionService } from '../../services/session.js'
6+
import { appRoutes } from '../app-routes.js'
77

88
export const Body = Shade<{ style?: Partial<CSSStyleDeclaration>; injector?: Injector }>({
99
customElementName: 'shade-app-body',

frontend/src/components/breadcrumbs.tsx renamed to frontend/src/components/layout/breadcrumbs.tsx

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { createComponent, LocationService, Shade } from '@furystack/shades'
33
import { cssVariableTheme } from '@furystack/shades-common-components'
44
import { GitHubRepository as GitHubRepositoryModel, ServiceDefinition, StackDefinition } from 'common'
55

6-
import { StackCraftNestedRouteLink } from './app-routes.js'
6+
import { StackCraftNestedRouteLink } from '../app-routes.js'
77

88
type BreadcrumbSegment = {
99
label: string
@@ -103,12 +103,17 @@ type EntityNameResolverProps = {
103103
const EntityNameResolver = Shade<EntityNameResolverProps>({
104104
customElementName: 'shade-entity-name-resolver',
105105
css: {
106-
display: 'flex',
107-
alignItems: 'center',
108-
gap: '6px',
109-
fontSize: '13px',
110-
color: cssVariableTheme.text.secondary,
111-
flexWrap: 'wrap',
106+
'& ol': {
107+
display: 'flex',
108+
alignItems: 'center',
109+
gap: '6px',
110+
fontSize: '13px',
111+
color: cssVariableTheme.text.secondary,
112+
flexWrap: 'wrap',
113+
listStyle: 'none',
114+
margin: '0',
115+
padding: '0',
116+
},
112117
'& a': {
113118
color: cssVariableTheme.text.secondary,
114119
textDecoration: 'none',
@@ -151,21 +156,25 @@ const EntityNameResolver = Shade<EntityNameResolverProps>({
151156

152157
return (
153158
<nav aria-label="Breadcrumb">
154-
{resolved.map((segment, index) => {
155-
const isLast = index === resolved.length - 1
156-
return (
157-
<span>
158-
{index > 0 ? <span className="breadcrumb-separator"> / </span> : null}
159-
{segment.href && !isLast ? (
160-
<StackCraftNestedRouteLink href={segment.href} params={segment.params as Record<string, string>}>
161-
{segment.label}
162-
</StackCraftNestedRouteLink>
163-
) : (
164-
<span className={isLast ? 'breadcrumb-current' : ''}>{segment.label}</span>
165-
)}
166-
</span>
167-
)
168-
})}
159+
<ol>
160+
{resolved.map((segment, index) => {
161+
const isLast = index === resolved.length - 1
162+
return (
163+
<li>
164+
{index > 0 ? <span className="breadcrumb-separator"> / </span> : null}
165+
{segment.href && !isLast ? (
166+
<StackCraftNestedRouteLink href={segment.href} params={segment.params as Record<string, string>}>
167+
{segment.label}
168+
</StackCraftNestedRouteLink>
169+
) : (
170+
<span className={isLast ? 'breadcrumb-current' : ''} {...(isLast ? { 'aria-current': 'page' } : {})}>
171+
{segment.label}
172+
</span>
173+
)}
174+
</li>
175+
)
176+
})}
177+
</ol>
169178
</nav>
170179
)
171180
},
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import type { Injector } from '@furystack/inject'
22
import { createComponent, Shade } from '@furystack/shades'
33
import { AppBar, AppBarLink, Button, DrawerToggleButton, Icon, icons } from '@furystack/shades-common-components'
4-
import { SessionService } from '../services/session.js'
4+
import { SessionService } from '../../services/session.js'
55
import { Breadcrumbs } from './breadcrumbs.js'
6-
import { ThemeSwitch } from './theme-switch/index.js'
6+
import { ThemeSwitch } from '../theme-switch/index.js'
77

88
export type HeaderProps = {
99
title: string
@@ -42,7 +42,7 @@ export const Header = Shade<HeaderProps>({
4242
{sessionState === 'authenticated' ? <Breadcrumbs /> : null}
4343
<div className="spacer" />
4444
<div className="actions">
45-
<ThemeSwitch variant="outlined" />
45+
<ThemeSwitch variant="outlined" size="small" />
4646
{sessionState === 'authenticated' ? (
4747
<Button
4848
variant="outlined"

0 commit comments

Comments
 (0)