refactor: implement phase 7 test/tooling simplification with tests

This commit is contained in:
Matthew Breedlove
2026-03-04 01:39:48 -05:00
parent fc09688879
commit 79b8e4e2f5
10 changed files with 318 additions and 393 deletions
+20 -32
View File
@@ -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
View File
@@ -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"
},
+19 -16
View File
@@ -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 () => {
+9 -50
View File
@@ -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)');
}
});
});
+9 -49
View File
@@ -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)');
}
});
});
+27 -96
View File
@@ -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
});
});
+9 -50
View File
@@ -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)');
}
});
});
+27 -97
View File
@@ -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
View File
@@ -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'] } });