Added tests for web/src/lib

This commit is contained in:
bwilliams committed 2026-10-16 16:29:08 +01:00
1 parent 67e2c96ca7
commit 60643546f6
5 files changed
+172 -2

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
import { expect, test } from 'vitest';
import type { ClassificationCode } from '@/types/Opportunities';
import { applyClassificationChanges } from './classifications';
const code = (id: number, value: string): ClassificationCode => ({
id,
is_cpv: true,
code: value,
classification: `Code ${value}`,
});
test('removes ids, adds codes and orders by code then id', () => {
const before = [code(1, '03000000'), code(2, '72000000'), code(3, '45000000')];
const after = applyClassificationChanges(before, {
added: [code(4, '09000000'), code(5, '03000000')],
removed: [2],
});
expect(after.map((c) => c.id)).toEqual([1, 5, 4, 3]);
expect(before).toHaveLength(3);
});
test('an added code replaces one with the same id', () => {
const after = applyClassificationChanges([code(1, '03000000')], {
added: [code(1, '03000001')],
removed: [],
});
expect(after).toEqual([code(1, '03000001')]);
});
+87
View File
@@ -0,0 +1,87 @@
import { describe, expect, test } from 'vitest';
import type { Annotation } from '@/types/Annotations';
import type { OpportunityFilters } from './filterOpportunities';
import {
activeFilterCount,
emptyFilters,
filterOpportunities,
NOT_MARKED,
statusLabel,
statusOptions,
} from './filterOpportunities';
import { testOpportunity } from './testOpportunity';
const opportunities = [
testOpportunity('a', {
title: 'Cloud hosting',
status: 'active',
sme_suitable: true,
classifications: [{ id: 1, is_cpv: true, code: '72000000', classification: 'IT' }],
latest_release_date: '2026-10-01T12:00:00Z',
}),
testOpportunity('b', {
title: 'Road repairs',
status: 'complete',
latest_release_date: '2026-10-03T12:00:00Z',
}),
testOpportunity('c', {
title: 'Cloud migration',
status: 'planned',
latest_release_date: '2026-10-02T12:00:00Z',
}),
];
const annotations: Record<string, Annotation> = {
a: { uuid: 'a', marks: ['interested'], hidden: false },
b: { uuid: 'b', marks: [], hidden: true },
c: { uuid: 'c', marks: [], hidden: false },
};
const annotationFor = (uuid: string): Annotation =>
annotations[uuid] ?? { uuid, marks: [], hidden: false };
const filtered = (filters: Partial<OpportunityFilters>): string[] =>
filterOpportunities(opportunities, { ...emptyFilters(), ...filters }, annotationFor).map(
(opportunity) => opportunity.uuid,
);
describe('filterOpportunities', () => {
test('hides hidden opportunities unless asked, newest release first', () => {
expect(filtered({})).toEqual(['c', 'a']);
expect(filtered({ showHidden: true })).toEqual(['b', 'c', 'a']);
});
test.each<{ name: string; filters: Partial<OpportunityFilters>; want: string[] }>([
{ name: 'title, ignoring case and spaces', filters: { title: ' CLOUD ' }, want: ['c', 'a'] },
{ name: 'statuses', filters: { statuses: new Set(['active', 'planned']) }, want: ['c', 'a'] },
{ name: 'a mark', filters: { marks: new Set(['interested']) }, want: ['a'] },
{ name: 'no marks', filters: { marks: new Set([NOT_MARKED]) }, want: ['c'] },
{ name: 'classification', filters: { classificationIds: new Set([1]) }, want: ['a'] },
{ name: 'SME suitable', filters: { smeSuitable: true }, want: ['a'] },
])('filters by $name', ({ filters, want }) => {
expect(filtered(filters)).toEqual(want);
});
test('combines filters', () => {
expect(filtered({ title: 'cloud', smeSuitable: true })).toEqual(['a']);
expect(filtered({ title: 'road', showHidden: true, smeSuitable: true })).toEqual([]);
});
});
test('activeFilterCount counts the filters in use', () => {
expect(activeFilterCount(emptyFilters())).toBe(0);
expect(
activeFilterCount({ ...emptyFilters(), title: ' ', smeSuitable: true, showHidden: true }),
).toBe(2);
});
test('statusOptions lists the known statuses, then others sorted', () => {
const options = statusOptions([
testOpportunity('x', { status: 'zzz' }),
testOpportunity('y', { status: 'aaa' }),
testOpportunity('z', { status: 'active' }),
]);
expect(options.slice(-2)).toEqual(['aaa', 'zzz']);
expect(options[0]).toBe('planning');
expect(statusLabel('active')).toBe('Active');
});
+18
View File
@@ -0,0 +1,18 @@
import { expect, test } from 'vitest';
import { mergeOpportunities } from './opportunities';
import { testOpportunity } from './testOpportunity';
test('replaces opportunities in place and adds new ones at the end', () => {
const list = [testOpportunity('a'), testOpportunity('b')];
const merged = mergeOpportunities(list, [
testOpportunity('c'),
testOpportunity('a', { title: 'Changed' }),
]);
expect(merged.map((o) => [o.uuid, o.title])).toEqual([
['a', 'Changed'],
['b', 'Opportunity b'],
['c', 'Opportunity c'],
]);
expect(list[0]?.title).toBe('Opportunity a');
});
+37
View File
@@ -0,0 +1,37 @@
import type { Opportunity } from '@/types/Opportunities';
/** An opportunity for tests, with overrides for the fields a test is about. */
export function testOpportunity(uuid: string, overrides: Partial<Opportunity> = {}): Opportunity {
return {
uuid,
uuid_type: 'ocid',
title: `Opportunity ${uuid}`,
status: 'active',
description: '',
buyer_name: 'Example Council',
buyer_id: 'GB-EXAMPLE',
classifications: [],
main_category: 'services',
procurement_method: 'open',
multi_lot: false,
sme_suitable: false,
vcse_suitable: false,
start_date: null,
end_date: null,
gross_value: 0,
has_documents: false,
source: 'FindATender',
latest_release_date: '2026-10-01T12:00:00Z',
releases: null,
marks: {
uuid,
hidden: false,
skipped: false,
interested: false,
bid_submitted: false,
won: false,
lost: false,
},
...overrides,
};
}
+1 -2
View File
@@ -26,10 +26,9 @@ export default defineConfig(({ mode }) => {
}, },
// Vitest. vite-plugin-solid would run tests in jsdom, which isn't // Vitest. vite-plugin-solid would run tests in jsdom, which isn't
// installed: nothing tested yet renders, and Solid's reactivity works // installed: nothing tested yet renders, and Solid's reactivity works
// in Node as it is. passWithNoTests until the first tests are added. // in Node as it is.
test: { test: {
environment: 'node', environment: 'node',
passWithNoTests: true,
}, },
}; };
}); });