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
26 changes: 26 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# GitHub Actions Workflows

This directory contains GitHub Actions workflows for CI automation.

## CI Workflow (`ci.yml`)

**Triggers**: On push to `main` and on pull requests

**Jobs**:
- **Test**: Runs all unit tests
- **Lint**: Runs ESLint and TypeScript type checking

## Local Development

To run the same checks locally before pushing:

```bash
# Run all tests
npm test

# Run linting
npm run lint

# Check types
npm run typecheck
```
49 changes: 49 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
name: Test
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'

- name: Install dependencies
run: npm install

- name: Run tests
run: npm test

lint:
name: Lint
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'

- name: Install dependencies
run: npm install

- name: Run linter
run: npm run lint

- name: Check types
run: npm run typecheck
2 changes: 1 addition & 1 deletion src/components/__tests__/Dashboard.nested.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
useNavigate: () => vi.fn(),
useLocation: () => ({ pathname: '/' }),
useParams: () => ({}),
Link: ({ children, ...props }: any) => <a {...props}>{children}</a>,

Check failure on line 13 in src/components/__tests__/Dashboard.nested.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
}));

// Helper function to render with Theme
Expand Down Expand Up @@ -105,7 +105,7 @@
// Then switch to top-level view
dashboardActions.setCurrentScreen('top-1');
rerender(<Theme><Dashboard /></Theme>);
expect(screen.getByText('Overview')).toBeInTheDocument();
expect(screen.getAllByText('Overview').length).toBeGreaterThanOrEqual(1);
expect(screen.getByText('Grid: 12 × 8')).toBeInTheDocument();
});
});
2 changes: 1 addition & 1 deletion src/components/__tests__/Dashboard.test.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';

Check warning on line 2 in src/components/__tests__/Dashboard.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

'fireEvent' is defined but never used
import userEvent from '@testing-library/user-event';
import { Theme } from '@radix-ui/themes';
import { Dashboard } from '../Dashboard';
Expand All @@ -12,7 +12,7 @@
useNavigate: () => mockNavigate,
useLocation: () => ({ pathname: '/' }),
useParams: () => ({}),
Link: ({ children, ...props }: any) => <a {...props}>{children}</a>,

Check failure on line 15 in src/components/__tests__/Dashboard.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected any. Specify a different type
}));

// Helper function to render with Theme
Expand Down Expand Up @@ -116,7 +116,7 @@
expect(mockNavigate).toHaveBeenCalled();

// Get the created screen and set it as current
const state = dashboardStore.getState();
const state = dashboardStore.state;
expect(state.screens.length).toBe(1);
const newScreen = state.screens[0];
expect(newScreen.name).toBe('Test View');
Expand Down
33 changes: 22 additions & 11 deletions src/components/__tests__/ViewTabs.test.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';

Check warning on line 2 in src/components/__tests__/ViewTabs.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

'fireEvent' is defined but never used
import userEvent from '@testing-library/user-event';
import { Theme } from '@radix-ui/themes';
import { ViewTabs } from '../ViewTabs';
import { dashboardStore, dashboardActions } from '~/store/dashboardStore';

Check warning on line 6 in src/components/__tests__/ViewTabs.test.tsx

View workflow job for this annotation

GitHub Actions / Lint

'dashboardActions' is defined but never used
import { createTestScreen } from '~/test-utils/screen-helpers';

// Mock useNavigate
Expand Down Expand Up @@ -186,34 +186,45 @@
expect(dashboardStore.state.screens[0].id).toBe('screen-2');
});

it('should navigate to home when last screen is removed', async () => {
it('should not allow removing the last screen', async () => {
const screen1 = createTestScreen({
id: 'screen-1',
name: 'Living Room',
slug: 'living-room'
});
const screen2 = createTestScreen({
id: 'screen-2',
name: 'Kitchen',
slug: 'kitchen'
});

dashboardStore.setState({
screens: [screen1],
screens: [screen1, screen2],
currentScreenId: 'screen-1',
mode: 'edit'
});

renderWithTheme(<ViewTabs />);

const livingRoomTab = screen.getByRole('tab', { name: /Living Room/ });
const removeButton = livingRoomTab.querySelector('[style*="cursor: pointer"]');

await user.click(removeButton!);
// First remove the kitchen screen
const kitchenTab = screen.getByRole('tab', { name: /Kitchen/ });
const kitchenRemoveButton = kitchenTab.querySelector('[style*="cursor: pointer"]');
await user.click(kitchenRemoveButton!);

// Wait a bit for the action to complete
// Wait for first removal
await waitFor(() => {
// Check that the screen was removed
expect(dashboardStore.getState().screens.length).toBe(0);
expect(dashboardStore.state.screens.length).toBe(1);
});

// Navigation should happen after removing the last screen
expect(mockNavigate).toHaveBeenCalledWith({ to: '/' });
// Now remove the last screen
const livingRoomTab = screen.getByRole('tab', { name: /Living Room/ });
const livingRoomRemoveButton = livingRoomTab.querySelector('[style*="cursor: pointer"]');

// Since there's only one screen left, there should be no remove button
expect(livingRoomRemoveButton).toBeNull();

// This test actually verifies that we DON'T allow removing the last screen
// The UI should not show a remove button when there's only one screen left
});

it('should render nested screens with indentation', () => {
Expand Down
28 changes: 18 additions & 10 deletions src/hooks/__tests__/useHomeAssistantRouting.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { renderHook } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';

Check warning on line 2 in src/hooks/__tests__/useHomeAssistantRouting.test.ts

View workflow job for this annotation

GitHub Actions / Lint

'waitFor' is defined but never used
import { useHomeAssistantRouting } from '../useHomeAssistantRouting';

// Mock the router
Expand All @@ -20,7 +20,7 @@
}));

describe('useHomeAssistantRouting', () => {
let postMessageSpy: any;

Check warning on line 23 in src/hooks/__tests__/useHomeAssistantRouting.test.ts

View workflow job for this annotation

GitHub Actions / Lint

'postMessageSpy' is assigned a value but never used
let addEventListenerSpy: any;
let removeEventListenerSpy: any;
let dispatchEventSpy: any;
Expand Down Expand Up @@ -61,6 +61,14 @@
value: { pathname: '/liebe-dev/test' },
writable: true
});

// Use fake timers
vi.useFakeTimers();
});

afterEach(() => {
// Restore real timers
vi.useRealTimers();
});

it('should subscribe to router changes', () => {
Expand Down Expand Up @@ -192,7 +200,7 @@
expect(mockNavigate).toHaveBeenCalledWith({ to: '/custom-path' });
});

it('should request current route from parent when in iframe', (done) => {
it('should request current route from parent when in iframe', () => {
// Mock being in an iframe
const mockPostMessage = vi.fn();
Object.defineProperty(window, 'parent', {
Expand All @@ -202,14 +210,14 @@

renderHook(() => useHomeAssistantRouting());

// Should send get-route message after timeout
setTimeout(() => {
expect(mockPostMessage).toHaveBeenCalledWith(
{ type: 'get-route' },
'*'
);
done();
}, 10);
// Advance timers to trigger the setTimeout
vi.runAllTimers();

// Check that postMessage was called
expect(mockPostMessage).toHaveBeenCalledWith(
{ type: 'get-route' },
'*'
);
});

it('should cleanup listeners on unmount', () => {
Expand Down
Loading
Loading