feat: add writing analytics dashboard and daily word goal tracking

This commit is contained in:
2026-06-11 20:44:16 +05:30
parent 50f4f62575
commit 2389d4f297
19 changed files with 1161 additions and 61 deletions
@@ -0,0 +1,73 @@
import { describe, it, expect, beforeEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { WritingAnalyticsDialog } from '@/components/modals/WritingAnalyticsDialog';
import { useAppStore } from '@/stores/app-store';
import { useEditorStore } from '@/stores/editor-store';
import { useSettingsStore } from '@/stores/settings-store';
describe('WritingAnalyticsDialog', () => {
beforeEach(() => {
// Reset stores
useAppStore.setState({ modal: { kind: 'writing-analytics' } } as any);
useSettingsStore.setState({ dailyGoal: 10 } as any);
useEditorStore.setState({
activeId: 'test-id',
buffers: new Map([
[
'test-id',
{
id: 'test-id',
path: 'test.md',
content: 'The quick brown fox jumps over the lazy dog. A smart programmer writes code.',
dirty: false,
},
],
]),
} as any);
});
it('renders title and descriptions', () => {
render(<WritingAnalyticsDialog />);
expect(screen.getByText(/writing analytics/i)).toBeInTheDocument();
expect(screen.getByText(/real-time readability/i)).toBeInTheDocument();
});
it('computes metrics from editor content correctly', () => {
render(<WritingAnalyticsDialog />);
// Content has 14 words: "The quick brown fox jumps over the lazy dog. A smart programmer writes code."
// Sentences: 2
// Paragraphs: 1
expect(screen.getByText('14')).toBeInTheDocument(); // Words count
expect(screen.getByText('2')).toBeInTheDocument(); // Sentences count
expect(screen.getByText('1')).toBeInTheDocument(); // Paragraphs count
});
it('updates daily word goal when input changes', async () => {
render(<WritingAnalyticsDialog />);
const input = screen.getByRole('spinbutton') as HTMLInputElement;
expect(input.value).toBe('10');
// Change target goal to 500
fireEvent.change(input, { target: { value: '500' } });
expect(useSettingsStore.getState().dailyGoal).toBe(500);
});
it('displays word frequency cloud tags', () => {
render(<WritingAnalyticsDialog />);
// "quick", "brown", "fox", "jumps", "lazy", "dog", "smart", "programmer", "writes", "code"
// (should exclude stop words like "the", "a", "over")
expect(screen.getByText('quick')).toBeInTheDocument();
expect(screen.getByText('lazy')).toBeInTheDocument();
expect(screen.getByText('programmer')).toBeInTheDocument();
expect(screen.queryByText('over')).toBeNull(); // stop word
});
it('closes when close button is clicked', async () => {
render(<WritingAnalyticsDialog />);
const closeButtons = await screen.findAllByRole('button', { name: /close/i });
const close = closeButtons[0];
await userEvent.click(close);
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
});
+7 -1
View File
@@ -24,7 +24,7 @@ describe('Sidebar', () => {
useEditorStore.setState({ buffers: new Map(), activeId: null });
});
it('renders without crashing with empty state in both sections', () => {
it('renders without crashing with empty state in all sections', () => {
render(
<ThemeProvider defaultTheme="dark" attribute="class">
<Sidebar />
@@ -32,6 +32,9 @@ describe('Sidebar', () => {
);
expect(screen.getByText('Files')).toBeInTheDocument();
expect(screen.getByText('Outline')).toBeInTheDocument();
expect(screen.getByText('Snippets')).toBeInTheDocument();
expect(screen.getByText('Templates')).toBeInTheDocument();
expect(screen.getByText('Git')).toBeInTheDocument();
expect(screen.getByText(/no folder opened/i)).toBeInTheDocument();
expect(screen.getByText(/no file open/i)).toBeInTheDocument();
});
@@ -56,6 +59,9 @@ describe('Sidebar', () => {
expect(screen.getByText('Files')).toBeInTheDocument();
expect(screen.getByText('Outline')).toBeInTheDocument();
expect(screen.getByText('Snippets')).toBeInTheDocument();
expect(screen.getByText('Templates')).toBeInTheDocument();
expect(screen.getByText('Git')).toBeInTheDocument();
expect(screen.getByText('README.md')).toBeInTheDocument();
});
+11
View File
@@ -33,4 +33,15 @@ describe('useEditorStore', () => {
useEditorStore.getState().closeBuffer('file-1');
expect(useEditorStore.getState().buffers.has('file-1')).toBe(false);
});
it('renames a buffer', () => {
useEditorStore.getState().openBuffer('file-1', '/foo.md', 'some content');
useEditorStore.getState().renameBuffer('file-1', 'file-2', '/bar.md');
const oldBuf = useEditorStore.getState().buffers.get('file-1');
const newBuf = useEditorStore.getState().buffers.get('file-2');
expect(oldBuf).toBeUndefined();
expect(newBuf?.content).toBe('some content');
expect(newBuf?.path).toBe('/bar.md');
expect(useEditorStore.getState().activeId).toBe('file-2');
});
});