Skip to content

Commit

Permalink
add prettier
Browse files Browse the repository at this point in the history
  • Loading branch information
lifeiscontent committed Dec 26, 2024
1 parent 014deaa commit 6d04696
Show file tree
Hide file tree
Showing 19 changed files with 221 additions and 208 deletions.
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
dist
coverage
node_modules
pnpm-lock.yaml
4 changes: 4 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"singleQuote": true,
"trailingComma": "all"
}
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ A simple, functional and immutable todo application built with TypeScript demons
The project follows a modular architecture with clear separation of concerns:

### `/src`

- `todo.ts`: Core domain model defining the `Todo` interface
- `store.ts`: Generic store interface for state management
- `app_store.ts`: Todo-specific store implementation with state management and actions
Expand All @@ -25,16 +26,19 @@ The project follows a modular architecture with clear separation of concerns:
The application follows these key architectural principles:

1. **Immutable State Management**

- All state updates create new state objects rather than mutating existing ones
- State changes are handled through a centralized store
- Typed actions for predictable state mutations

2. **Event-Based Architecture**

- Uses a publish/subscribe pattern for state updates
- Components subscribe to state changes and update accordingly
- Strong typing ensures type-safe event handling

3. **Functional Components**

- UI components are created using pure functions
- Side effects are isolated and managed through callbacks
- Clear separation between view logic and state management
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"coverage": "vitest run --coverage"
"coverage": "vitest run --coverage",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
"devDependencies": {
"@vitest/coverage-v8": "2.1.8",
"jsdom": "25.0.1",
"typescript": "~5.7.2",
"prettier": "3.4.2",
"typescript": "5.7.2",
"vite": "6.0.6",
"vitest": "2.1.8"
},
Expand Down
12 changes: 11 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 7 additions & 7 deletions src/app_store.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('AppStore', () => {
store.addTodo('First todo');
store.addTodo('Second todo');
const [todo1] = store.getState().todos;

store.toggleTodo(todo1.id);
const state = store.getState();
expect(state.todos[0].completed).toBe(true); // First todo toggled
Expand All @@ -56,7 +56,7 @@ describe('AppStore', () => {
const store = AppStore.create({ todos: [] });
store.addTodo('Test todo');
const todo = store.getState().todos[0];

store.removeTodo(todo.id);
expect(store.getState().todos).toHaveLength(0);
});
Expand All @@ -71,7 +71,7 @@ describe('AppStore', () => {
it('should notify subscribers of state changes', () => {
const store = AppStore.create({ todos: [] });
const actions: AppStore.Action[] = [];

store.subscribe((action) => {
actions.push(action);
});
Expand Down Expand Up @@ -142,23 +142,23 @@ describe('AppStore', () => {
const store = AppStore.create({ todos: [] });
const subscriber1 = (_action: AppStore.Action) => {};
const subscriber2 = (_action: AppStore.Action) => {};

// Subscribe both subscribers
const unsubscribe1 = store.subscribe(subscriber1);
const unsubscribe2 = store.subscribe(subscriber2);

// Unsubscribe the first one
unsubscribe1();

// Add a todo to verify subscriber2 still gets notifications
store.addTodo('Test todo');

// Try to unsubscribe subscriber1 again (should handle -1 index)
expect(() => unsubscribe1()).not.toThrow();

// Unsubscribe subscriber2
unsubscribe2();

// Try to unsubscribe both again (should handle empty subscribers array)
expect(() => {
unsubscribe1();
Expand Down
26 changes: 13 additions & 13 deletions src/app_store.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import { Store } from "./store";
import { Todo } from "./todo";
import { Store } from './store';
import { Todo } from './todo';

/** Application state type */
export type State = { todos: Todo[] };

/** Action types for state mutations */
export type Action =
| { type: "todoAdded"; todo: Todo }
| { type: "todoUpdated"; todo: Todo }
| { type: "todoRemoved"; id: Todo["id"] };
| { type: 'todoAdded'; todo: Todo }
| { type: 'todoUpdated'; todo: Todo }
| { type: 'todoRemoved'; id: Todo['id'] };

/** Store type with todo-specific actions */
export type t = Store<State, Action> & {
/** Adds a new todo item */
addTodo: (text: string) => void;
/** Removes a todo item by id */
removeTodo: (id: Todo["id"]) => void;
removeTodo: (id: Todo['id']) => void;
/** Toggles the completed state of a todo item */
toggleTodo: (id: Todo["id"]) => void;
toggleTodo: (id: Todo['id']) => void;
};

/**
Expand All @@ -40,7 +40,7 @@ export function create(initialState: State): t {
publish(action);
};

const findTodo = (id: Todo["id"]) => state.todos.find((t) => t.id === id);
const findTodo = (id: Todo['id']) => state.todos.find((t) => t.id === id);

return {
getState: (): Readonly<State> => state,
Expand All @@ -61,10 +61,10 @@ export function create(initialState: State): t {
};
update(
{ ...state, todos: [...state.todos, newTodo] },
{ type: "todoAdded", todo: newTodo }
{ type: 'todoAdded', todo: newTodo },
);
},
toggleTodo: (id: Todo["id"]) => {
toggleTodo: (id: Todo['id']) => {
const todo = findTodo(id);
if (!todo) return;

Expand All @@ -74,14 +74,14 @@ export function create(initialState: State): t {
...state,
todos: state.todos.map((t) => (t.id === id ? updatedTodo : t)),
},
{ type: "todoUpdated", todo: updatedTodo }
{ type: 'todoUpdated', todo: updatedTodo },
);
},
removeTodo: (id: Todo["id"]) => {
removeTodo: (id: Todo['id']) => {
if (!findTodo(id)) return;
update(
{ ...state, todos: state.todos.filter((t) => t.id !== id) },
{ type: "todoRemoved", id }
{ type: 'todoRemoved', id },
);
},
};
Expand Down
96 changes: 38 additions & 58 deletions src/app_view.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { describe, it, expect, vi } from 'vitest';
import * as AppView from './app_view';
import type { Todo } from './todo';
import type { Action } from './app_store';
import type * as AppStore from './app_store';

describe('AppView', () => {
it('should create app view with all elements', () => {
const mockStore = {
getState: () => ({ todos: [] }),
subscribe: (_dispatch: (action: Action) => void) => () => {},
function createMockStore(initialTodos: Todo[] = []) {
return {
getState: () => ({ todos: initialTodos }),
subscribe: vi.fn(
(_dispatch: (action: AppStore.Action) => void) => () => {},
),
addTodo: vi.fn(),
removeTodo: vi.fn(),
toggleTodo: vi.fn(),
};
}

const view = AppView.create(mockStore);
it('should create app view with all elements', () => {
const view = AppView.create(createMockStore());

expect(view.container).toBeInstanceOf(HTMLDivElement);
expect(view.heading).toBeInstanceOf(HTMLHeadingElement);
Expand All @@ -25,15 +29,9 @@ describe('AppView', () => {
});

it('should handle form submission', () => {
const mockStore = {
getState: () => ({ todos: [] }),
subscribe: (_dispatch: (action: Action) => void) => () => {},
addTodo: vi.fn(),
removeTodo: vi.fn(),
toggleTodo: vi.fn(),
};

const mockStore = createMockStore();
const view = AppView.create(mockStore);

view.input.value = 'Test todo';
view.form.dispatchEvent(new Event('submit'));

Expand All @@ -42,31 +40,19 @@ describe('AppView', () => {
});

it('should not add empty todos', () => {
const mockStore = {
getState: () => ({ todos: [] }),
subscribe: (_dispatch: (action: Action) => void) => () => {},
addTodo: vi.fn(),
removeTodo: vi.fn(),
toggleTodo: vi.fn(),
};

const mockStore = createMockStore();
const view = AppView.create(mockStore);

view.input.value = ' ';
view.form.dispatchEvent(new Event('submit'));

expect(mockStore.addTodo).not.toHaveBeenCalled();
});

it('should handle todo actions', () => {
const mockStore = {
getState: () => ({ todos: [] }),
subscribe: vi.fn(),
addTodo: vi.fn(),
removeTodo: vi.fn(),
toggleTodo: vi.fn(),
};

const mockStore = createMockStore();
AppView.create(mockStore);

const subscriber = mockStore.subscribe.mock.calls[0][0];

// Test todoAdded action
Expand All @@ -76,7 +62,9 @@ describe('AppView', () => {

// Test todoUpdated action
subscriber({ type: 'todoUpdated', todo: { ...todo, completed: true } });
const checkbox = document.querySelector<HTMLInputElement>('input[type="checkbox"]');
const checkbox = document.querySelector<HTMLInputElement>(
'input[type="checkbox"]',
);
expect(checkbox?.checked).toBe(true);

// Test todoRemoved action
Expand All @@ -85,15 +73,9 @@ describe('AppView', () => {
});

it('should handle unknown action', () => {
const mockStore = {
getState: () => ({ todos: [] }),
subscribe: vi.fn(),
addTodo: vi.fn(),
removeTodo: vi.fn(),
toggleTodo: vi.fn(),
};

const mockStore = createMockStore();
AppView.create(mockStore);

const subscriber = mockStore.subscribe.mock.calls[0][0];

expect(() => {
Expand All @@ -102,28 +84,18 @@ describe('AppView', () => {
});

it('should initialize with existing todos', () => {
const todo: Todo = { id: Symbol(), text: 'Existing todo', completed: false };
const mockStore = {
getState: () => ({ todos: [todo] }),
subscribe: (_dispatch: (action: Action) => void) => () => {},
addTodo: vi.fn(),
removeTodo: vi.fn(),
toggleTodo: vi.fn(),
const todo: Todo = {
id: Symbol(),
text: 'Existing todo',
completed: false,
};
AppView.create(createMockStore([todo]));

AppView.create(mockStore);
expect(document.querySelectorAll('li').length).toBe(1);
});

it('should handle non-existent todo item views', () => {
const mockStore = {
getState: () => ({ todos: [] }),
subscribe: vi.fn(),
addTodo: vi.fn(),
removeTodo: vi.fn(),
toggleTodo: vi.fn(),
};

const mockStore = createMockStore();
const view = AppView.create(mockStore);
const subscriber = mockStore.subscribe.mock.calls[0][0];

Expand All @@ -132,14 +104,22 @@ describe('AppView', () => {
subscriber({ type: 'todoAdded', todo });

// Test todoUpdated action with non-existent todo
const nonExistentTodo: Todo = { id: Symbol(), text: 'Non-existent', completed: false };
const nonExistentTodo: Todo = {
id: Symbol(),
text: 'Non-existent',
completed: false,
};
subscriber({ type: 'todoUpdated', todo: nonExistentTodo });
expect(view.ul.children.length).toBe(1);
expect(view.ul.children[0].querySelector('label')?.textContent?.trim()).toBe('Test todo');
expect(
view.ul.children[0].querySelector('label')?.textContent?.trim(),
).toBe('Test todo');

// Test todoRemoved action with non-existent todo
subscriber({ type: 'todoRemoved', id: nonExistentTodo.id });
expect(view.ul.children.length).toBe(1);
expect(view.ul.children[0].querySelector('label')?.textContent?.trim()).toBe('Test todo');
expect(
view.ul.children[0].querySelector('label')?.textContent?.trim(),
).toBe('Test todo');
});
});
Loading

0 comments on commit 6d04696

Please sign in to comment.