mirror of
https://github.com/tiennm99/ccstatusline.git
synced 2026-09-03 12:21:21 +00:00
refactor: implement phase 7 test/tooling simplification with tests
This commit is contained in:
+20
-32
@@ -7,6 +7,23 @@ import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||
import globals from 'globals';
|
||||
|
||||
const importResolverSettings = {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: ['./tsconfig.json'],
|
||||
alwaysTryTypes: true,
|
||||
noWarnOnMultipleProjects: true
|
||||
},
|
||||
node: {
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json']
|
||||
}
|
||||
},
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx']
|
||||
},
|
||||
'import/external-module-folders': ['node_modules', 'node_modules/@types']
|
||||
};
|
||||
|
||||
export default ts.config([
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
@@ -38,21 +55,7 @@ export default ts.config([
|
||||
}
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: ['./tsconfig.json'],
|
||||
alwaysTryTypes: true,
|
||||
noWarnOnMultipleProjects: true
|
||||
},
|
||||
parcel2: {},
|
||||
node: {
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json']
|
||||
}
|
||||
},
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx']
|
||||
},
|
||||
'import/external-module-folders': ['node_modules', 'node_modules/@types']
|
||||
...importResolverSettings
|
||||
},
|
||||
rules: {
|
||||
'no-control-regex': 'off', // We intentionally match ANSI escape sequences
|
||||
@@ -119,22 +122,7 @@ export default ts.config([
|
||||
'react-hooks': reactHooksPlugin
|
||||
},
|
||||
settings: {
|
||||
...{
|
||||
'import/resolver': {
|
||||
typescript: {
|
||||
project: ['./tsconfig.json'],
|
||||
alwaysTryTypes: true,
|
||||
noWarnOnMultipleProjects: true
|
||||
},
|
||||
node: {
|
||||
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json']
|
||||
}
|
||||
},
|
||||
'import/parsers': {
|
||||
'@typescript-eslint/parser': ['.ts', '.tsx']
|
||||
},
|
||||
'import/external-module-folders': ['node_modules', 'node_modules/@types']
|
||||
},
|
||||
...importResolverSettings,
|
||||
react: {
|
||||
version: 'detect'
|
||||
}
|
||||
@@ -156,4 +144,4 @@ export default ts.config([
|
||||
'!eslint.config.js'
|
||||
]
|
||||
}
|
||||
]);
|
||||
]);
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
"postbuild": "bun run scripts/replace-version.ts",
|
||||
"example": "cat scripts/payload.example.json | bun start",
|
||||
"prepublishOnly": "bun run build",
|
||||
"lint": "bun tsc --noEmit; eslint . --config eslint.config.js --max-warnings=999999 --fix",
|
||||
"lint": "bun tsc --noEmit && eslint . --config eslint.config.js --max-warnings=999999 --fix",
|
||||
"docs": "typedoc",
|
||||
"docs:clean": "rm -rf docs"
|
||||
},
|
||||
|
||||
@@ -25,22 +25,25 @@ function makeLines(...lineItems: WidgetItem[][]): WidgetItem[][] {
|
||||
}
|
||||
|
||||
describe('usage prefetch', () => {
|
||||
it('detects when usage widgets are present', () => {
|
||||
const lines = makeLines(
|
||||
[{ id: '1', type: 'model' }],
|
||||
[{ id: '2', type: 'block-timer' }]
|
||||
);
|
||||
|
||||
expect(hasUsageDependentWidgets(lines)).toBe(true);
|
||||
});
|
||||
|
||||
it('does not detect usage requirement for non-usage widgets', () => {
|
||||
const lines = makeLines(
|
||||
[{ id: '1', type: 'model' }],
|
||||
[{ id: '2', type: 'git-branch' }]
|
||||
);
|
||||
|
||||
expect(hasUsageDependentWidgets(lines)).toBe(false);
|
||||
it.each([
|
||||
{
|
||||
expected: true,
|
||||
lines: makeLines(
|
||||
[{ id: '1', type: 'model' }],
|
||||
[{ id: '2', type: 'block-timer' }]
|
||||
),
|
||||
name: 'detects when usage widgets are present'
|
||||
},
|
||||
{
|
||||
expected: false,
|
||||
lines: makeLines(
|
||||
[{ id: '1', type: 'model' }],
|
||||
[{ id: '2', type: 'git-branch' }]
|
||||
),
|
||||
name: 'does not detect usage requirement for non-usage widgets'
|
||||
}
|
||||
])('$name', ({ expected, lines }) => {
|
||||
expect(hasUsageDependentWidgets(lines)).toBe(expected);
|
||||
});
|
||||
|
||||
it('fetches usage data once when at least one usage widget exists', async () => {
|
||||
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
} from '../../utils/usage';
|
||||
import { BlockResetTimerWidget } from '../BlockResetTimer';
|
||||
|
||||
import { runUsageTimerEditorSuite } from './helpers/usage-widget-suites';
|
||||
|
||||
vi.mock('../../utils/usage', () => ({
|
||||
formatUsageDuration: vi.fn(),
|
||||
getUsageErrorMessage: vi.fn(),
|
||||
@@ -35,17 +37,6 @@ describe('BlockResetTimerWidget', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('supports raw value and exposes progress/invert keybinds', () => {
|
||||
const widget = new BlockResetTimerWidget();
|
||||
|
||||
expect(widget.getDisplayName()).toBe('Block Reset Timer');
|
||||
expect(widget.supportsRawValue()).toBe(true);
|
||||
expect(widget.getCustomKeybinds()).toEqual([
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
|
||||
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders preview using block-style reset format', () => {
|
||||
const widget = new BlockResetTimerWidget();
|
||||
|
||||
@@ -121,47 +112,15 @@ describe('BlockResetTimerWidget', () => {
|
||||
expect(render(widget, { id: 'reset', type: 'reset-timer', rawValue: true }, { usageData: {} })).toBe('3hr 45m');
|
||||
});
|
||||
|
||||
it('clears invert metadata when cycling back to time mode', () => {
|
||||
const widget = new BlockResetTimerWidget();
|
||||
const updated = widget.handleEditorAction('toggle-progress', {
|
||||
runUsageTimerEditorSuite({
|
||||
baseItem: { id: 'reset', type: 'reset-timer' },
|
||||
createWidget: () => new BlockResetTimerWidget(),
|
||||
expectedDisplayName: 'Block Reset Timer',
|
||||
expectedModifierText: '(short bar, inverted)',
|
||||
modifierItem: {
|
||||
id: 'reset',
|
||||
type: 'reset-timer',
|
||||
metadata: {
|
||||
display: 'progress-short',
|
||||
invert: 'true'
|
||||
}
|
||||
});
|
||||
|
||||
expect(updated?.metadata?.display).toBe('time');
|
||||
expect(updated?.metadata?.invert).toBeUndefined();
|
||||
});
|
||||
|
||||
it('cycles display modes in the expected order', () => {
|
||||
const widget = new BlockResetTimerWidget();
|
||||
const base: WidgetItem = { id: 'reset', type: 'reset-timer' };
|
||||
|
||||
const first = widget.handleEditorAction('toggle-progress', base);
|
||||
const second = widget.handleEditorAction('toggle-progress', first ?? base);
|
||||
const third = widget.handleEditorAction('toggle-progress', second ?? base);
|
||||
|
||||
expect(first?.metadata?.display).toBe('progress');
|
||||
expect(second?.metadata?.display).toBe('progress-short');
|
||||
expect(third?.metadata?.display).toBe('time');
|
||||
});
|
||||
|
||||
it('toggles invert metadata and shows editor modifiers', () => {
|
||||
const widget = new BlockResetTimerWidget();
|
||||
const base: WidgetItem = { id: 'reset', type: 'reset-timer' };
|
||||
|
||||
const inverted = widget.handleEditorAction('toggle-invert', base);
|
||||
const cleared = widget.handleEditorAction('toggle-invert', inverted ?? base);
|
||||
|
||||
expect(inverted?.metadata?.invert).toBe('true');
|
||||
expect(cleared?.metadata?.invert).toBe('false');
|
||||
expect(widget.getEditorDisplay(base).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay({
|
||||
...base,
|
||||
metadata: { display: 'progress-short', invert: 'true' }
|
||||
}).modifierText).toBe('(short bar, inverted)');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
} from '../../utils/usage';
|
||||
import { BlockTimerWidget } from '../BlockTimer';
|
||||
|
||||
import { runUsageTimerEditorSuite } from './helpers/usage-widget-suites';
|
||||
|
||||
vi.mock('../../utils/usage', () => ({
|
||||
formatUsageDuration: vi.fn(),
|
||||
resolveUsageWindowWithFallback: vi.fn()
|
||||
@@ -32,16 +34,6 @@ describe('BlockTimerWidget', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('supports raw value and exposes progress/invert keybinds', () => {
|
||||
const widget = new BlockTimerWidget();
|
||||
|
||||
expect(widget.supportsRawValue()).toBe(true);
|
||||
expect(widget.getCustomKeybinds()).toEqual([
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
|
||||
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders elapsed time in time mode', () => {
|
||||
const widget = new BlockTimerWidget();
|
||||
const item: WidgetItem = { id: 'block', type: 'block-timer' };
|
||||
@@ -108,47 +100,15 @@ describe('BlockTimerWidget', () => {
|
||||
expect(render(widget, { id: 'block', type: 'block-timer', rawValue: true }, { usageData: {} })).toBe('2hr');
|
||||
});
|
||||
|
||||
it('clears invert metadata when cycling back to time mode', () => {
|
||||
const widget = new BlockTimerWidget();
|
||||
const updated = widget.handleEditorAction('toggle-progress', {
|
||||
runUsageTimerEditorSuite({
|
||||
baseItem: { id: 'block', type: 'block-timer' },
|
||||
createWidget: () => new BlockTimerWidget(),
|
||||
expectedDisplayName: 'Block Timer',
|
||||
expectedModifierText: '(progress bar, inverted)',
|
||||
modifierItem: {
|
||||
id: 'block',
|
||||
type: 'block-timer',
|
||||
metadata: {
|
||||
display: 'progress-short',
|
||||
invert: 'true'
|
||||
}
|
||||
});
|
||||
|
||||
expect(updated?.metadata?.display).toBe('time');
|
||||
expect(updated?.metadata?.invert).toBeUndefined();
|
||||
});
|
||||
|
||||
it('cycles display modes in the expected order', () => {
|
||||
const widget = new BlockTimerWidget();
|
||||
const base: WidgetItem = { id: 'block', type: 'block-timer' };
|
||||
|
||||
const first = widget.handleEditorAction('toggle-progress', base);
|
||||
const second = widget.handleEditorAction('toggle-progress', first ?? base);
|
||||
const third = widget.handleEditorAction('toggle-progress', second ?? base);
|
||||
|
||||
expect(first?.metadata?.display).toBe('progress');
|
||||
expect(second?.metadata?.display).toBe('progress-short');
|
||||
expect(third?.metadata?.display).toBe('time');
|
||||
});
|
||||
|
||||
it('toggles invert metadata and shows editor modifiers', () => {
|
||||
const widget = new BlockTimerWidget();
|
||||
const base: WidgetItem = { id: 'block', type: 'block-timer' };
|
||||
|
||||
const inverted = widget.handleEditorAction('toggle-invert', base);
|
||||
const cleared = widget.handleEditorAction('toggle-invert', inverted ?? base);
|
||||
|
||||
expect(inverted?.metadata?.invert).toBe('true');
|
||||
expect(cleared?.metadata?.invert).toBe('false');
|
||||
expect(widget.getEditorDisplay(base).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay({
|
||||
...base,
|
||||
metadata: { display: 'progress', invert: 'true' }
|
||||
}).modifierText).toBe('(progress bar, inverted)');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,5 @@
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
@@ -12,6 +9,8 @@ import type { WidgetItem } from '../../types/Widget';
|
||||
import { getUsageErrorMessage } from '../../utils/usage';
|
||||
import { SessionUsageWidget } from '../SessionUsage';
|
||||
|
||||
import { runUsagePercentWidgetSuite } from './helpers/usage-widget-suites';
|
||||
|
||||
vi.mock('../../utils/usage', () => ({
|
||||
getUsageErrorMessage: vi.fn(),
|
||||
makeUsageProgressBar: vi.fn((percent: number, width = 15) => `[bar:${percent.toFixed(1)}:${width}]`)
|
||||
@@ -24,106 +23,38 @@ function render(widget: SessionUsageWidget, item: WidgetItem, context: RenderCon
|
||||
}
|
||||
|
||||
describe('SessionUsageWidget', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('exposes progress and invert keybinds', () => {
|
||||
const widget = new SessionUsageWidget();
|
||||
|
||||
expect(widget.supportsRawValue()).toBe(true);
|
||||
expect(widget.getCustomKeybinds()).toEqual([
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
|
||||
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders percentage text in time mode', () => {
|
||||
const widget = new SessionUsageWidget();
|
||||
expect(render(widget, { id: 'session', type: 'session-usage' }, { usageData: { sessionUsage: 23.45 } })).toBe('Session: 23.4%');
|
||||
});
|
||||
|
||||
it('renders short inverted progress mode', () => {
|
||||
const widget = new SessionUsageWidget();
|
||||
const item: WidgetItem = {
|
||||
runUsagePercentWidgetSuite({
|
||||
baseItem: { id: 'session', type: 'session-usage' },
|
||||
createWidget: () => new SessionUsageWidget(),
|
||||
errorMessageMock: mockGetUsageErrorMessage,
|
||||
expectedModifierText: '(short bar, inverted)',
|
||||
expectedProgress: 'Session: [bar:76.5:16] 76.5%',
|
||||
expectedRawProgress: '[bar:23.4:32] 23.4%',
|
||||
expectedRawTime: '23.4%',
|
||||
expectedTime: 'Session: 23.4%',
|
||||
modifierItem: {
|
||||
id: 'session',
|
||||
type: 'session-usage',
|
||||
metadata: {
|
||||
display: 'progress-short',
|
||||
invert: 'true'
|
||||
}
|
||||
};
|
||||
|
||||
expect(render(widget, item, { usageData: { sessionUsage: 23.45 } })).toBe('Session: [bar:76.5:16] 76.5%');
|
||||
});
|
||||
|
||||
it('renders raw text mode without label', () => {
|
||||
const widget = new SessionUsageWidget();
|
||||
|
||||
expect(render(widget, { id: 'session', type: 'session-usage', rawValue: true }, { usageData: { sessionUsage: 23.45 } })).toBe('23.4%');
|
||||
});
|
||||
|
||||
it('renders raw progress mode without label', () => {
|
||||
const widget = new SessionUsageWidget();
|
||||
const item: WidgetItem = {
|
||||
metadata: { display: 'progress-short', invert: 'true' }
|
||||
},
|
||||
progressItem: {
|
||||
id: 'session',
|
||||
type: 'session-usage',
|
||||
metadata: { display: 'progress-short', invert: 'true' }
|
||||
},
|
||||
rawProgressItem: {
|
||||
id: 'session',
|
||||
type: 'session-usage',
|
||||
rawValue: true,
|
||||
metadata: { display: 'progress' }
|
||||
};
|
||||
|
||||
expect(render(widget, item, { usageData: { sessionUsage: 23.45 } })).toBe('[bar:23.4:32] 23.4%');
|
||||
});
|
||||
|
||||
it('shows usage error text when API call fails', () => {
|
||||
const widget = new SessionUsageWidget();
|
||||
|
||||
mockGetUsageErrorMessage.mockReturnValue('[Timeout]');
|
||||
|
||||
expect(render(widget, { id: 'session', type: 'session-usage' }, { usageData: { error: 'timeout' } })).toBe('[Timeout]');
|
||||
});
|
||||
|
||||
it('clears invert metadata when cycling back to time mode', () => {
|
||||
const widget = new SessionUsageWidget();
|
||||
const updated = widget.handleEditorAction('toggle-progress', {
|
||||
},
|
||||
rawTimeItem: {
|
||||
id: 'session',
|
||||
type: 'session-usage',
|
||||
metadata: {
|
||||
display: 'progress-short',
|
||||
invert: 'true'
|
||||
}
|
||||
});
|
||||
|
||||
expect(updated?.metadata?.display).toBe('time');
|
||||
expect(updated?.metadata?.invert).toBeUndefined();
|
||||
});
|
||||
|
||||
it('cycles display modes in the expected order', () => {
|
||||
const widget = new SessionUsageWidget();
|
||||
const base: WidgetItem = { id: 'session', type: 'session-usage' };
|
||||
|
||||
const first = widget.handleEditorAction('toggle-progress', base);
|
||||
const second = widget.handleEditorAction('toggle-progress', first ?? base);
|
||||
const third = widget.handleEditorAction('toggle-progress', second ?? base);
|
||||
|
||||
expect(first?.metadata?.display).toBe('progress');
|
||||
expect(second?.metadata?.display).toBe('progress-short');
|
||||
expect(third?.metadata?.display).toBe('time');
|
||||
});
|
||||
|
||||
it('toggles invert metadata and shows editor modifiers', () => {
|
||||
const widget = new SessionUsageWidget();
|
||||
const base: WidgetItem = { id: 'session', type: 'session-usage' };
|
||||
|
||||
const inverted = widget.handleEditorAction('toggle-invert', base);
|
||||
const cleared = widget.handleEditorAction('toggle-invert', inverted ?? base);
|
||||
|
||||
expect(inverted?.metadata?.invert).toBe('true');
|
||||
expect(cleared?.metadata?.invert).toBe('false');
|
||||
expect(widget.getEditorDisplay(base).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay({
|
||||
...base,
|
||||
metadata: { display: 'progress-short', invert: 'true' }
|
||||
}).modifierText).toBe('(short bar, inverted)');
|
||||
rawValue: true
|
||||
},
|
||||
render,
|
||||
usageField: 'sessionUsage',
|
||||
usageValue: 23.45
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,8 @@ import {
|
||||
} from '../../utils/usage';
|
||||
import { WeeklyResetTimerWidget } from '../WeeklyResetTimer';
|
||||
|
||||
import { runUsageTimerEditorSuite } from './helpers/usage-widget-suites';
|
||||
|
||||
vi.mock('../../utils/usage', () => ({
|
||||
formatUsageDuration: vi.fn(),
|
||||
getUsageErrorMessage: vi.fn(),
|
||||
@@ -35,17 +37,6 @@ describe('WeeklyResetTimerWidget', () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('supports raw value and exposes progress/invert keybinds', () => {
|
||||
const widget = new WeeklyResetTimerWidget();
|
||||
|
||||
expect(widget.getDisplayName()).toBe('Weekly Reset Timer');
|
||||
expect(widget.supportsRawValue()).toBe(true);
|
||||
expect(widget.getCustomKeybinds()).toEqual([
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
|
||||
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders preview using weekly reset format', () => {
|
||||
const widget = new WeeklyResetTimerWidget();
|
||||
|
||||
@@ -121,47 +112,15 @@ describe('WeeklyResetTimerWidget', () => {
|
||||
expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer', rawValue: true }, { usageData: {} })).toBe('120hr 15m');
|
||||
});
|
||||
|
||||
it('clears invert metadata when cycling back to time mode', () => {
|
||||
const widget = new WeeklyResetTimerWidget();
|
||||
const updated = widget.handleEditorAction('toggle-progress', {
|
||||
runUsageTimerEditorSuite({
|
||||
baseItem: { id: 'weekly-reset', type: 'weekly-reset-timer' },
|
||||
createWidget: () => new WeeklyResetTimerWidget(),
|
||||
expectedDisplayName: 'Weekly Reset Timer',
|
||||
expectedModifierText: '(short bar, inverted)',
|
||||
modifierItem: {
|
||||
id: 'weekly-reset',
|
||||
type: 'weekly-reset-timer',
|
||||
metadata: {
|
||||
display: 'progress-short',
|
||||
invert: 'true'
|
||||
}
|
||||
});
|
||||
|
||||
expect(updated?.metadata?.display).toBe('time');
|
||||
expect(updated?.metadata?.invert).toBeUndefined();
|
||||
});
|
||||
|
||||
it('cycles display modes in the expected order', () => {
|
||||
const widget = new WeeklyResetTimerWidget();
|
||||
const base: WidgetItem = { id: 'weekly-reset', type: 'weekly-reset-timer' };
|
||||
|
||||
const first = widget.handleEditorAction('toggle-progress', base);
|
||||
const second = widget.handleEditorAction('toggle-progress', first ?? base);
|
||||
const third = widget.handleEditorAction('toggle-progress', second ?? base);
|
||||
|
||||
expect(first?.metadata?.display).toBe('progress');
|
||||
expect(second?.metadata?.display).toBe('progress-short');
|
||||
expect(third?.metadata?.display).toBe('time');
|
||||
});
|
||||
|
||||
it('toggles invert metadata and shows editor modifiers', () => {
|
||||
const widget = new WeeklyResetTimerWidget();
|
||||
const base: WidgetItem = { id: 'weekly-reset', type: 'weekly-reset-timer' };
|
||||
|
||||
const inverted = widget.handleEditorAction('toggle-invert', base);
|
||||
const cleared = widget.handleEditorAction('toggle-invert', inverted ?? base);
|
||||
|
||||
expect(inverted?.metadata?.invert).toBe('true');
|
||||
expect(cleared?.metadata?.invert).toBe('false');
|
||||
expect(widget.getEditorDisplay(base).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay({
|
||||
...base,
|
||||
metadata: { display: 'progress-short', invert: 'true' }
|
||||
}).modifierText).toBe('(short bar, inverted)');
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,5 @@
|
||||
import {
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
@@ -12,6 +9,8 @@ import type { WidgetItem } from '../../types/Widget';
|
||||
import { getUsageErrorMessage } from '../../utils/usage';
|
||||
import { WeeklyUsageWidget } from '../WeeklyUsage';
|
||||
|
||||
import { runUsagePercentWidgetSuite } from './helpers/usage-widget-suites';
|
||||
|
||||
vi.mock('../../utils/usage', () => ({
|
||||
getUsageErrorMessage: vi.fn(),
|
||||
makeUsageProgressBar: vi.fn((percent: number, width = 15) => `[bar:${percent.toFixed(1)}:${width}]`)
|
||||
@@ -24,107 +23,38 @@ function render(widget: WeeklyUsageWidget, item: WidgetItem, context: RenderCont
|
||||
}
|
||||
|
||||
describe('WeeklyUsageWidget', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('exposes progress and invert keybinds', () => {
|
||||
const widget = new WeeklyUsageWidget();
|
||||
|
||||
expect(widget.supportsRawValue()).toBe(true);
|
||||
expect(widget.getCustomKeybinds()).toEqual([
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
|
||||
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('renders percentage text in time mode', () => {
|
||||
const widget = new WeeklyUsageWidget();
|
||||
|
||||
expect(render(widget, { id: 'weekly', type: 'weekly-usage' }, { usageData: { weeklyUsage: 42.06 } })).toBe('Weekly: 42.1%');
|
||||
});
|
||||
|
||||
it('renders full inverted progress mode', () => {
|
||||
const widget = new WeeklyUsageWidget();
|
||||
const item: WidgetItem = {
|
||||
runUsagePercentWidgetSuite({
|
||||
baseItem: { id: 'weekly', type: 'weekly-usage' },
|
||||
createWidget: () => new WeeklyUsageWidget(),
|
||||
errorMessageMock: mockGetUsageErrorMessage,
|
||||
expectedModifierText: '(progress bar, inverted)',
|
||||
expectedProgress: 'Weekly: [bar:57.9:32] 57.9%',
|
||||
expectedRawProgress: '[bar:42.1:16] 42.1%',
|
||||
expectedRawTime: '42.1%',
|
||||
expectedTime: 'Weekly: 42.1%',
|
||||
modifierItem: {
|
||||
id: 'weekly',
|
||||
type: 'weekly-usage',
|
||||
metadata: {
|
||||
display: 'progress',
|
||||
invert: 'true'
|
||||
}
|
||||
};
|
||||
|
||||
expect(render(widget, item, { usageData: { weeklyUsage: 42.06 } })).toBe('Weekly: [bar:57.9:32] 57.9%');
|
||||
});
|
||||
|
||||
it('renders raw text mode without label', () => {
|
||||
const widget = new WeeklyUsageWidget();
|
||||
|
||||
expect(render(widget, { id: 'weekly', type: 'weekly-usage', rawValue: true }, { usageData: { weeklyUsage: 42.06 } })).toBe('42.1%');
|
||||
});
|
||||
|
||||
it('renders raw progress mode without label', () => {
|
||||
const widget = new WeeklyUsageWidget();
|
||||
const item: WidgetItem = {
|
||||
metadata: { display: 'progress', invert: 'true' }
|
||||
},
|
||||
progressItem: {
|
||||
id: 'weekly',
|
||||
type: 'weekly-usage',
|
||||
metadata: { display: 'progress', invert: 'true' }
|
||||
},
|
||||
rawProgressItem: {
|
||||
id: 'weekly',
|
||||
type: 'weekly-usage',
|
||||
rawValue: true,
|
||||
metadata: { display: 'progress-short' }
|
||||
};
|
||||
|
||||
expect(render(widget, item, { usageData: { weeklyUsage: 42.06 } })).toBe('[bar:42.1:16] 42.1%');
|
||||
});
|
||||
|
||||
it('shows usage error text when API call fails', () => {
|
||||
const widget = new WeeklyUsageWidget();
|
||||
|
||||
mockGetUsageErrorMessage.mockReturnValue('[Timeout]');
|
||||
|
||||
expect(render(widget, { id: 'weekly', type: 'weekly-usage' }, { usageData: { error: 'timeout' } })).toBe('[Timeout]');
|
||||
});
|
||||
|
||||
it('clears invert metadata when cycling back to time mode', () => {
|
||||
const widget = new WeeklyUsageWidget();
|
||||
const updated = widget.handleEditorAction('toggle-progress', {
|
||||
},
|
||||
rawTimeItem: {
|
||||
id: 'weekly',
|
||||
type: 'weekly-usage',
|
||||
metadata: {
|
||||
display: 'progress-short',
|
||||
invert: 'true'
|
||||
}
|
||||
});
|
||||
|
||||
expect(updated?.metadata?.display).toBe('time');
|
||||
expect(updated?.metadata?.invert).toBeUndefined();
|
||||
});
|
||||
|
||||
it('cycles display modes in the expected order', () => {
|
||||
const widget = new WeeklyUsageWidget();
|
||||
const base: WidgetItem = { id: 'weekly', type: 'weekly-usage' };
|
||||
|
||||
const first = widget.handleEditorAction('toggle-progress', base);
|
||||
const second = widget.handleEditorAction('toggle-progress', first ?? base);
|
||||
const third = widget.handleEditorAction('toggle-progress', second ?? base);
|
||||
|
||||
expect(first?.metadata?.display).toBe('progress');
|
||||
expect(second?.metadata?.display).toBe('progress-short');
|
||||
expect(third?.metadata?.display).toBe('time');
|
||||
});
|
||||
|
||||
it('toggles invert metadata and shows editor modifiers', () => {
|
||||
const widget = new WeeklyUsageWidget();
|
||||
const base: WidgetItem = { id: 'weekly', type: 'weekly-usage' };
|
||||
|
||||
const inverted = widget.handleEditorAction('toggle-invert', base);
|
||||
const cleared = widget.handleEditorAction('toggle-invert', inverted ?? base);
|
||||
|
||||
expect(inverted?.metadata?.invert).toBe('true');
|
||||
expect(cleared?.metadata?.invert).toBe('false');
|
||||
expect(widget.getEditorDisplay(base).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay({
|
||||
...base,
|
||||
metadata: { display: 'progress', invert: 'true' }
|
||||
}).modifierText).toBe('(progress bar, inverted)');
|
||||
rawValue: true
|
||||
},
|
||||
render,
|
||||
usageField: 'weeklyUsage',
|
||||
usageValue: 42.06
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
import {
|
||||
beforeEach,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import type { RenderContext } from '../../../types/RenderContext';
|
||||
import type {
|
||||
CustomKeybind,
|
||||
WidgetEditorDisplay,
|
||||
WidgetItem
|
||||
} from '../../../types/Widget';
|
||||
|
||||
interface UsageWidgetLike {
|
||||
getCustomKeybinds(): CustomKeybind[];
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay;
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null;
|
||||
supportsRawValue(): boolean;
|
||||
}
|
||||
|
||||
interface UsagePercentWidgetSuiteConfig<TWidget extends UsageWidgetLike> {
|
||||
baseItem: WidgetItem;
|
||||
createWidget: () => TWidget;
|
||||
errorMessageMock: { mockReturnValue: (value: string) => void };
|
||||
expectedModifierText: string;
|
||||
expectedProgress: string;
|
||||
expectedRawProgress: string;
|
||||
expectedRawTime: string;
|
||||
expectedTime: string;
|
||||
modifierItem: WidgetItem;
|
||||
progressItem: WidgetItem;
|
||||
rawProgressItem: WidgetItem;
|
||||
rawTimeItem: WidgetItem;
|
||||
render: (widget: TWidget, item: WidgetItem, context?: RenderContext) => string | null;
|
||||
usageField: 'sessionUsage' | 'weeklyUsage';
|
||||
usageValue: number;
|
||||
}
|
||||
|
||||
interface UsageTimerEditorSuiteConfig<TWidget extends UsageWidgetLike & { getDisplayName(): string }> {
|
||||
baseItem: WidgetItem;
|
||||
createWidget: () => TWidget;
|
||||
expectedDisplayName: string;
|
||||
expectedModifierText: string;
|
||||
modifierItem: WidgetItem;
|
||||
}
|
||||
|
||||
const EXPECTED_USAGE_KEYBINDS: CustomKeybind[] = [
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
|
||||
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
|
||||
];
|
||||
|
||||
function getUsageContext(field: 'sessionUsage' | 'weeklyUsage', value: number): RenderContext {
|
||||
return field === 'sessionUsage'
|
||||
? { usageData: { sessionUsage: value } }
|
||||
: { usageData: { weeklyUsage: value } };
|
||||
}
|
||||
|
||||
export function runUsagePercentWidgetSuite<TWidget extends UsageWidgetLike>(config: UsagePercentWidgetSuiteConfig<TWidget>): void {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('exposes progress and invert keybinds', () => {
|
||||
const widget = config.createWidget();
|
||||
|
||||
expect(widget.supportsRawValue()).toBe(true);
|
||||
expect(widget.getCustomKeybinds()).toEqual(EXPECTED_USAGE_KEYBINDS);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
expected: config.expectedTime,
|
||||
item: config.baseItem,
|
||||
name: 'renders percentage text in time mode'
|
||||
},
|
||||
{
|
||||
expected: config.expectedProgress,
|
||||
item: config.progressItem,
|
||||
name: 'renders progress mode'
|
||||
},
|
||||
{
|
||||
expected: config.expectedRawTime,
|
||||
item: config.rawTimeItem,
|
||||
name: 'renders raw text mode without label'
|
||||
},
|
||||
{
|
||||
expected: config.expectedRawProgress,
|
||||
item: config.rawProgressItem,
|
||||
name: 'renders raw progress mode without label'
|
||||
}
|
||||
])('$name', ({ expected, item }) => {
|
||||
const widget = config.createWidget();
|
||||
const context = getUsageContext(config.usageField, config.usageValue);
|
||||
|
||||
expect(config.render(widget, item, context)).toBe(expected);
|
||||
});
|
||||
|
||||
it('shows usage error text when API call fails', () => {
|
||||
const widget = config.createWidget();
|
||||
|
||||
config.errorMessageMock.mockReturnValue('[Timeout]');
|
||||
expect(config.render(widget, config.baseItem, { usageData: { error: 'timeout' } })).toBe('[Timeout]');
|
||||
});
|
||||
|
||||
it('clears invert metadata when cycling back to time mode', () => {
|
||||
const widget = config.createWidget();
|
||||
const updated = widget.handleEditorAction('toggle-progress', {
|
||||
...config.baseItem,
|
||||
metadata: {
|
||||
display: 'progress-short',
|
||||
invert: 'true'
|
||||
}
|
||||
});
|
||||
|
||||
expect(updated?.metadata?.display).toBe('time');
|
||||
expect(updated?.metadata?.invert).toBeUndefined();
|
||||
});
|
||||
|
||||
it('cycles display modes in the expected order', () => {
|
||||
const widget = config.createWidget();
|
||||
|
||||
const first = widget.handleEditorAction('toggle-progress', config.baseItem);
|
||||
const second = widget.handleEditorAction('toggle-progress', first ?? config.baseItem);
|
||||
const third = widget.handleEditorAction('toggle-progress', second ?? config.baseItem);
|
||||
|
||||
expect(first?.metadata?.display).toBe('progress');
|
||||
expect(second?.metadata?.display).toBe('progress-short');
|
||||
expect(third?.metadata?.display).toBe('time');
|
||||
});
|
||||
|
||||
it('toggles invert metadata and shows editor modifiers', () => {
|
||||
const widget = config.createWidget();
|
||||
|
||||
const inverted = widget.handleEditorAction('toggle-invert', config.baseItem);
|
||||
const cleared = widget.handleEditorAction('toggle-invert', inverted ?? config.baseItem);
|
||||
|
||||
expect(inverted?.metadata?.invert).toBe('true');
|
||||
expect(cleared?.metadata?.invert).toBe('false');
|
||||
expect(widget.getEditorDisplay(config.baseItem).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay(config.modifierItem).modifierText).toBe(config.expectedModifierText);
|
||||
});
|
||||
}
|
||||
|
||||
export function runUsageTimerEditorSuite<TWidget extends UsageWidgetLike & { getDisplayName(): string }>(config: UsageTimerEditorSuiteConfig<TWidget>): void {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('supports raw value and exposes progress/invert keybinds', () => {
|
||||
const widget = config.createWidget();
|
||||
|
||||
expect(widget.getDisplayName()).toBe(config.expectedDisplayName);
|
||||
expect(widget.supportsRawValue()).toBe(true);
|
||||
expect(widget.getCustomKeybinds()).toEqual(EXPECTED_USAGE_KEYBINDS);
|
||||
});
|
||||
|
||||
it('clears invert metadata when cycling back to time mode', () => {
|
||||
const widget = config.createWidget();
|
||||
const updated = widget.handleEditorAction('toggle-progress', {
|
||||
...config.baseItem,
|
||||
metadata: {
|
||||
display: 'progress-short',
|
||||
invert: 'true'
|
||||
}
|
||||
});
|
||||
|
||||
expect(updated?.metadata?.display).toBe('time');
|
||||
expect(updated?.metadata?.invert).toBeUndefined();
|
||||
});
|
||||
|
||||
it('cycles display modes in the expected order', () => {
|
||||
const widget = config.createWidget();
|
||||
|
||||
const first = widget.handleEditorAction('toggle-progress', config.baseItem);
|
||||
const second = widget.handleEditorAction('toggle-progress', first ?? config.baseItem);
|
||||
const third = widget.handleEditorAction('toggle-progress', second ?? config.baseItem);
|
||||
|
||||
expect(first?.metadata?.display).toBe('progress');
|
||||
expect(second?.metadata?.display).toBe('progress-short');
|
||||
expect(third?.metadata?.display).toBe('time');
|
||||
});
|
||||
|
||||
it('toggles invert metadata and shows editor modifiers', () => {
|
||||
const widget = config.createWidget();
|
||||
|
||||
const inverted = widget.handleEditorAction('toggle-invert', config.baseItem);
|
||||
const cleared = widget.handleEditorAction('toggle-invert', inverted ?? config.baseItem);
|
||||
|
||||
expect(inverted?.metadata?.invert).toBe('true');
|
||||
expect(cleared?.metadata?.invert).toBe('false');
|
||||
expect(widget.getEditorDisplay(config.baseItem).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay(config.modifierItem).modifierText).toBe(config.expectedModifierText);
|
||||
});
|
||||
}
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vite';
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({ test: {} });
|
||||
export default defineConfig({ test: { include: ['src/**/*.test.ts', 'src/**/*.test.tsx'] } });
|
||||
Reference in New Issue
Block a user