diff --git a/keda/src/components/common/CommonComponents.test.ts b/keda/src/components/common/CommonComponents.test.ts new file mode 100644 index 0000000000..647cd97798 --- /dev/null +++ b/keda/src/components/common/CommonComponents.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from 'vitest'; +import { getCompletions, sortByCompletions } from './CommonComponents'; + +function job(completions?: number, parallelism?: number) { + return { spec: { completions, parallelism } } as any; +} + +describe('sortByCompletions', () => { + it('sorts by completions, not parallelism, when they disagree', () => { + // job1 has fewer completions but more parallelism than job2 — a completions-first + // sort must put job1 before job2, the opposite of what a parallelism-first sort would do. + expect(sortByCompletions(job(1, 5), job(5, 1))).toBeLessThan(0); + }); + + it('falls back to parallelism when completions are equal', () => { + expect(sortByCompletions(job(2, 1), job(2, 4))).toBeLessThan(0); + }); + + it('treats missing completions/parallelism as 0 instead of NaN', () => { + expect(sortByCompletions(job(undefined, undefined), job(1, 1))).toBeLessThan(0); + expect( + Number.isNaN(sortByCompletions(job(undefined, undefined), job(undefined, undefined))) + ).toBe(false); + }); +}); + +describe('getCompletions', () => { + it('renders both fields when set', () => { + expect(getCompletions(job(1, 5))).toBe('1/5'); + }); + + it('renders 0 instead of undefined for unset fields, matching how they sort', () => { + expect(getCompletions(job(undefined, undefined))).toBe('0/0'); + }); +}); diff --git a/keda/src/components/common/CommonComponents.tsx b/keda/src/components/common/CommonComponents.tsx index f25cfea7b8..9258d8734d 100644 --- a/keda/src/components/common/CommonComponents.tsx +++ b/keda/src/components/common/CommonComponents.tsx @@ -564,22 +564,22 @@ export interface JobsListRendererProps { noNamespaceFilter?: boolean; } +export function sortByCompletions(job1: Job, job2: Job) { + const completionsSorted = (job1.spec.completions ?? 0) - (job2.spec.completions ?? 0); + if (completionsSorted === 0) { + return (job1.spec.parallelism ?? 0) - (job2.spec.parallelism ?? 0); + } + return completionsSorted; +} + +export function getCompletions(job: Job) { + return `${job.spec.completions ?? 0}/${job.spec.parallelism ?? 0}`; +} + export function JobsListRenderer(props: JobsListRendererProps) { const { jobs, errors, hideColumns = [], reflectTableInURL = 'jobs', noNamespaceFilter } = props; const { t } = useTranslation(); - function getCompletions(job: Job) { - return `${job.spec.completions}/${job.spec.parallelism}`; - } - - function sortByCompletions(job1: Job, job2: Job) { - const parallelismSorted = job1.spec.parallelism - job2.spec.parallelism; - if (parallelismSorted === 0) { - return job1.spec.completions - job2.spec.completions; - } - return parallelismSorted; - } - return (