Files
hope-events/backend/tests/feedback.test.js
joshuaandClaude Opus 5.5 afcc14cbb3 Add post-event feedback forms
A Feedback step in the event wizard builds a survey that is sent at a
chosen time to everyone who checked in (or everyone registered), with
personal links and one reminder after 3 days. Answers can be anonymous.
Adds rating (1-5 stars) and multiple-choice field types, also usable on
attendee forms, and a Feedback summary report with response rate,
average ratings, choice breakdowns, comments and event comparison.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:28:29 +02:00

69 lines
3.1 KiB
JavaScript

const { cleanFields, cleanAnswer, summarise } = require('../src/utils/feedback');
describe('cleanFields', () => {
test('keeps labelled questions in order, tidying choices', () => {
const out = cleanFields([
{ type: 'rating', label: ' Overall ', isRequired: true },
{ type: 'text', label: '' },
{ type: 'multiple_choice', label: 'Best part', options: ['Worship', ' Talks ', 'Worship', ''] },
{ type: 'statement', label: 'Thanks!', isRequired: true },
]);
expect(out.map(f => [f.type, f.label, f.order])).toEqual([['rating', 'Overall', 0], ['multiple_choice', 'Best part', 1], ['statement', 'Thanks!', 2]]);
expect(out[1].options).toEqual(['Worship', 'Talks']);
expect(out[2].isRequired).toBe(false);
});
test('rejects bad forms', () => {
expect(() => cleanFields([{ type: 'stars', label: 'X' }])).toThrow(/unknown type/);
expect(() => cleanFields([{ type: 'multiple_choice', label: 'X', options: ['only one'] }])).toThrow(/two choices/);
expect(() => cleanFields([{ type: 'statement', label: 'Hello' }])).toThrow(/at least one question/);
});
});
describe('cleanAnswer', () => {
test('ratings are whole stars from 1 to 5', () => {
expect(cleanAnswer({ type: 'rating', label: 'R' }, '4')).toBe('4');
expect(() => cleanAnswer({ type: 'rating', label: 'R' }, '6')).toThrow();
expect(() => cleanAnswer({ type: 'rating', label: 'R' }, '3.5')).toThrow();
});
test('multiple choice must be one of the options', () => {
const f = { type: 'multiple_choice', label: 'M', options: ['A', 'B'] };
expect(cleanAnswer(f, 'B')).toBe('B');
expect(() => cleanAnswer(f, 'C')).toThrow();
});
test('required questions need an answer; optional ones may be blank', () => {
expect(() => cleanAnswer({ type: 'text', label: 'Why?', isRequired: true }, ' ')).toThrow(/Please answer/);
expect(cleanAnswer({ type: 'text', label: 'Why?' }, '')).toBeNull();
expect(cleanAnswer({ type: 'yes_no', label: 'Again?' }, 'YES')).toBe('yes');
});
});
describe('summarise', () => {
const fields = [
{ id: 'r', type: 'rating', label: 'Overall' },
{ id: 'm', type: 'multiple_choice', label: 'Best part', options: ['Worship', 'Talks'] },
{ id: 't', type: 'text', label: 'Comments' },
{ id: 's', type: 'statement', label: 'Thanks' },
];
const answers = [
{ fieldId: 'r', value: '5' }, { fieldId: 'r', value: '4' }, { fieldId: 'r', value: '4' },
{ fieldId: 'm', value: 'Talks' }, { fieldId: 'm', value: 'Talks' }, { fieldId: 'm', value: 'Worship' },
{ fieldId: 't', value: 'Loved it', name: 'Ann' },
];
test('averages ratings with a star distribution', () => {
const [r] = summarise(fields, answers);
expect(r.average).toBe(4.3);
expect(r.distribution.map(d => d.value)).toEqual([0, 0, 0, 2, 1]);
});
test('counts choices and lists text answers; skips statements', () => {
const out = summarise(fields, answers);
expect(out.length).toBe(3);
expect(out[1].counts).toEqual([{ label: 'Worship', value: 1 }, { label: 'Talks', value: 2 }]);
expect(out[2].texts).toEqual([{ value: 'Loved it', name: 'Ann' }]);
});
});