Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 137 additions & 135 deletions .cursor/rules/REST_SERVICE.mdc

Large diffs are not rendered by default.

194 changes: 97 additions & 97 deletions .cursor/rules/TESTING_GUIDELINES.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ The project uses Vitest for unit testing with workspace-based configuration:

```typescript
// vitest.config.mts
import { defineConfig } from 'vitest/config';
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
Expand Down Expand Up @@ -48,7 +48,7 @@ export default defineConfig({
},
],
},
});
})
```

### Playwright for E2E Tests
Expand All @@ -57,8 +57,8 @@ E2E tests use Playwright with the service auto-started:

```typescript
// playwright.config.ts
import type { PlaywrightTestConfig } from '@playwright/test';
import { devices } from '@playwright/test';
import type { PlaywrightTestConfig } from '@playwright/test'
import { devices } from '@playwright/test'

const config: PlaywrightTestConfig = {
testDir: 'e2e',
Expand All @@ -76,7 +76,7 @@ const config: PlaywrightTestConfig = {
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
],
};
}
```

## Unit Testing with Vitest
Expand All @@ -98,97 +98,97 @@ service/src/
Use `describe`, `it`, and `expect` from Vitest:

```typescript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'

describe('MyService', () => {
describe('methodName', () => {
it('should do something when condition', () => {
// Arrange
const input = 'test';
const input = 'test'

// Act
const result = myFunction(input);
const result = myFunction(input)

// Assert
expect(result).toBe('expected');
});
});
});
expect(result).toBe('expected')
})
})
})
```

### Testing Services

Test service methods with mocked dependencies:

```typescript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Injector } from '@furystack/inject';
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { Injector } from '@furystack/inject'

describe('SessionService', () => {
let injector: Injector;
let sessionService: SessionService;
let injector: Injector
let sessionService: SessionService

beforeEach(() => {
injector = new Injector();
injector = new Injector()
// Set up mocks
const mockApiClient = {
call: vi.fn(),
};
injector.setExplicitInstance(BoilerplateApiClient, mockApiClient);
sessionService = injector.getInstance(SessionService);
});
}
injector.setExplicitInstance(StackCraftApiClient, mockApiClient)
sessionService = injector.getInstance(SessionService)
})

it('should initialize with unauthenticated state', async () => {
expect(sessionService.state.getValue()).toBe('initializing');
});
});
expect(sessionService.state.getValue()).toBe('initializing')
})
})
```

### Mocking with Vitest

Use `vi.fn()` for function mocks and `vi.spyOn()` for spying:

```typescript
import { vi } from 'vitest';
import { vi } from 'vitest'

// Mock a function
const mockFn = vi.fn().mockReturnValue('mocked');
const mockFn = vi.fn().mockReturnValue('mocked')

// Mock with implementation
const mockFn = vi.fn().mockImplementation((arg) => `result: ${arg}`);
const mockFn = vi.fn().mockImplementation((arg) => `result: ${arg}`)

// Mock async function
const mockAsync = vi.fn().mockResolvedValue({ data: 'test' });
const mockAsync = vi.fn().mockResolvedValue({ data: 'test' })

// Spy on method
const spy = vi.spyOn(service, 'method');
const spy = vi.spyOn(service, 'method')

// Verify calls
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith('arg');
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalled()
expect(mockFn).toHaveBeenCalledWith('arg')
expect(mockFn).toHaveBeenCalledTimes(2)
```

### Testing Observable Values

Test ObservableValue subscriptions:

```typescript
import { ObservableValue } from '@furystack/utils';
import { ObservableValue } from '@furystack/utils'

describe('ObservableValue', () => {
it('should notify subscribers on value change', () => {
const observable = new ObservableValue<string>('initial');
const values: string[] = [];
const subscription = observable.subscribe((value) => values.push(value));
observable.setValue('updated');
expect(values).toEqual(['initial', 'updated']);
subscription.dispose();
});
});
const observable = new ObservableValue<string>('initial')
const values: string[] = []

const subscription = observable.subscribe((value) => values.push(value))
observable.setValue('updated')

expect(values).toEqual(['initial', 'updated'])

subscription.dispose()
})
})
```

## E2E Testing with Playwright
Expand All @@ -209,26 +209,26 @@ e2e/
Use Playwright's test API:

```typescript
import { expect, test } from '@playwright/test';
import { expect, test } from '@playwright/test'

test.describe('Feature Name', () => {
test('should do something', async ({ page }) => {
// Navigate
await page.goto('/');
await page.goto('/')

// Find elements
const element = page.locator('selector');
const element = page.locator('selector')

// Assert visibility
await expect(element).toBeVisible();
await expect(element).toBeVisible()

// Interact
await element.click();
await element.click()

// Assert result
await expect(page.locator('.result')).toHaveText('Expected');
});
});
await expect(page.locator('.result')).toHaveText('Expected')
})
})
```

### Locating Shades Components
Expand All @@ -238,58 +238,58 @@ Use shadow DOM component names as selectors:
```typescript
test('should interact with Shades components', async ({ page }) => {
// Locate by shadow DOM name
const loginForm = page.locator('shade-login form');
await expect(loginForm).toBeVisible();
const loginForm = page.locator('shade-login form')
await expect(loginForm).toBeVisible()

// Locate inputs within components
const usernameInput = loginForm.locator('input[name="userName"]');
const passwordInput = loginForm.locator('input[name="password"]');
const usernameInput = loginForm.locator('input[name="userName"]')
const passwordInput = loginForm.locator('input[name="password"]')

// Fill inputs
await usernameInput.type('testuser');
await passwordInput.type('password');
await usernameInput.type('testuser')
await passwordInput.type('password')

// Click buttons
const submitButton = page.locator('button', { hasText: 'Login' });
await submitButton.click();
});
const submitButton = page.locator('button', { hasText: 'Login' })
await submitButton.click()
})
```

### Authentication Flow Test

Example of testing login/logout:

```typescript
import { expect, test } from '@playwright/test';
import { expect, test } from '@playwright/test'

test.describe('Authentication', () => {
test('Login and logout roundtrip', async ({ page }) => {
await page.goto('/');
await page.goto('/')

// Wait for login form
const loginForm = page.locator('shade-login form');
await expect(loginForm).toBeVisible();
const loginForm = page.locator('shade-login form')
await expect(loginForm).toBeVisible()

// Fill credentials
await loginForm.locator('input[name="userName"]').type('testuser');
await loginForm.locator('input[name="password"]').type('password');
await loginForm.locator('input[name="userName"]').type('testuser')
await loginForm.locator('input[name="password"]').type('password')

// Submit
await page.locator('button', { hasText: 'Login' }).click();
await page.locator('button', { hasText: 'Login' }).click()

// Verify logged in state
const welcomeTitle = page.locator('hello-world div h2');
await expect(welcomeTitle).toBeVisible();
await expect(welcomeTitle).toHaveText('Hello, testuser !');
const welcomeTitle = page.locator('hello-world div h2')
await expect(welcomeTitle).toBeVisible()
await expect(welcomeTitle).toHaveText('Hello, testuser !')

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

// Verify logged out
await expect(page.locator('shade-login form')).toBeVisible();
});
});
await expect(page.locator('shade-login form')).toBeVisible()
})
})
```

### Waiting for Elements
Expand All @@ -298,14 +298,14 @@ Use Playwright's auto-waiting or explicit waits:

```typescript
// Auto-wait (recommended)
await expect(element).toBeVisible();
await expect(element).toBeVisible()

// Explicit wait
await page.waitForSelector('selector');
await page.waitForLoadState('networkidle');
await page.waitForSelector('selector')
await page.waitForLoadState('networkidle')

// Wait for response
await page.waitForResponse('**/api/endpoint');
await page.waitForResponse('**/api/endpoint')
```

## Running Tests
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/build-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ jobs:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: yarn install
- name: Prettier check
run: yarn prettier:check
- name: Format check
run: yarn format:check
- name: Build
run: yarn build
env:
Expand Down
28 changes: 28 additions & 0 deletions .github/workflows/check-changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Changelog checks
on:
push:
branches-ignore:
- 'release/**'
- 'master'
- 'develop'
pull_request:
branches:
- develop
jobs:
check:
name: Check changelog completion
timeout-minutes: 5
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Check changelog entries
run: yarn changelog check
env:
CI: true
3 changes: 1 addition & 2 deletions .github/workflows/check-version-bump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ jobs:
with:
fetch-depth: 0
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- name: Check version bumps
continue-on-error: true ## Set this to false once versioning has been set up
run: yarn version check
env:
CI: true
Loading
Loading