From 79b8e4e2f5708de9c24e24e74d8d8b2b8053abd2 Mon Sep 17 00:00:00 2001 From: Matthew Breedlove Date: Wed, 4 Mar 2026 01:39:48 -0500 Subject: [PATCH] refactor: implement phase 7 test/tooling simplification with tests --- eslint.config.js | 52 ++--- package.json | 2 +- src/utils/__tests__/usage-prefetch.test.ts | 35 ++-- src/widgets/__tests__/BlockResetTimer.test.ts | 59 +----- src/widgets/__tests__/BlockTimer.test.ts | 58 +----- src/widgets/__tests__/SessionUsage.test.ts | 123 +++-------- .../__tests__/WeeklyResetTimer.test.ts | 59 +----- src/widgets/__tests__/WeeklyUsage.test.ts | 124 +++-------- .../__tests__/helpers/usage-widget-suites.ts | 195 ++++++++++++++++++ vitest.config.ts | 4 +- 10 files changed, 318 insertions(+), 393 deletions(-) create mode 100644 src/widgets/__tests__/helpers/usage-widget-suites.ts diff --git a/eslint.config.js b/eslint.config.js index 4c0f267..affb520 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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' ] } -]); \ No newline at end of file +]); diff --git a/package.json b/package.json index 46314ad..a5f53dc 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/utils/__tests__/usage-prefetch.test.ts b/src/utils/__tests__/usage-prefetch.test.ts index f07c8ca..2b63e82 100644 --- a/src/utils/__tests__/usage-prefetch.test.ts +++ b/src/utils/__tests__/usage-prefetch.test.ts @@ -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 () => { diff --git a/src/widgets/__tests__/BlockResetTimer.test.ts b/src/widgets/__tests__/BlockResetTimer.test.ts index fc06588..15818ad 100644 --- a/src/widgets/__tests__/BlockResetTimer.test.ts +++ b/src/widgets/__tests__/BlockResetTimer.test.ts @@ -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)'); + } }); }); \ No newline at end of file diff --git a/src/widgets/__tests__/BlockTimer.test.ts b/src/widgets/__tests__/BlockTimer.test.ts index 1cc7c0d..f2cb28b 100644 --- a/src/widgets/__tests__/BlockTimer.test.ts +++ b/src/widgets/__tests__/BlockTimer.test.ts @@ -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)'); + } }); }); \ No newline at end of file diff --git a/src/widgets/__tests__/SessionUsage.test.ts b/src/widgets/__tests__/SessionUsage.test.ts index 12d7ad5..7139de2 100644 --- a/src/widgets/__tests__/SessionUsage.test.ts +++ b/src/widgets/__tests__/SessionUsage.test.ts @@ -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 }); }); \ No newline at end of file diff --git a/src/widgets/__tests__/WeeklyResetTimer.test.ts b/src/widgets/__tests__/WeeklyResetTimer.test.ts index ff60e2e..a83d80d 100644 --- a/src/widgets/__tests__/WeeklyResetTimer.test.ts +++ b/src/widgets/__tests__/WeeklyResetTimer.test.ts @@ -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)'); + } }); }); \ No newline at end of file diff --git a/src/widgets/__tests__/WeeklyUsage.test.ts b/src/widgets/__tests__/WeeklyUsage.test.ts index 6892b7b..5b42e12 100644 --- a/src/widgets/__tests__/WeeklyUsage.test.ts +++ b/src/widgets/__tests__/WeeklyUsage.test.ts @@ -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 }); }); \ No newline at end of file diff --git a/src/widgets/__tests__/helpers/usage-widget-suites.ts b/src/widgets/__tests__/helpers/usage-widget-suites.ts new file mode 100644 index 0000000..45e9b54 --- /dev/null +++ b/src/widgets/__tests__/helpers/usage-widget-suites.ts @@ -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 { + 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 { + 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(config: UsagePercentWidgetSuiteConfig): 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(config: UsageTimerEditorSuiteConfig): 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); + }); +} \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts index a1653c8..45b0c4c 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,4 +1,4 @@ /// -import { defineConfig } from 'vite'; +import { defineConfig } from 'vitest/config'; -export default defineConfig({ test: {} }); \ No newline at end of file +export default defineConfig({ test: { include: ['src/**/*.test.ts', 'src/**/*.test.tsx'] } }); \ No newline at end of file