Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {describe, expect, it} from 'vitest';
import {feedbackOptions} from './feedback-options';

describe('feedbackOptions', () => {
it('should export feedback options array', () => {
expect(feedbackOptions).toBeDefined();
expect(Array.isArray(feedbackOptions)).toBe(true);
});

it('should contain 4 feedback options', () => {
expect(feedbackOptions).toHaveLength(4);
});

it('should have correct structure for each option', () => {
feedbackOptions.forEach((option) => {
expect(option).toHaveProperty('id');
expect(option).toHaveProperty('localeKey');
expect(option).toHaveProperty('correspondingAnswer');
expect(typeof option.id).toBe('string');
expect(typeof option.localeKey).toBe('string');
});
});

it('should include "does_not_answer" option', () => {
const option = feedbackOptions.find(
(opt) => opt.correspondingAnswer === 'does_not_answer'
);

expect(option).toBeDefined();
expect(option?.id).toBe('does-not-answer');
expect(option?.localeKey).toBe(
'smart-snippet-feedback-reason-does-not-answer'
);
});

it('should include "partially_answers" option', () => {
const option = feedbackOptions.find(
(opt) => opt.correspondingAnswer === 'partially_answers'
);

expect(option).toBeDefined();
expect(option?.id).toBe('partially-answers');
expect(option?.localeKey).toBe(
'smart-snippet-feedback-reason-partially-answers'
);
});

it('should include "was_not_a_question" option', () => {
const option = feedbackOptions.find(
(opt) => opt.correspondingAnswer === 'was_not_a_question'
);

expect(option).toBeDefined();
expect(option?.id).toBe('was-not-a-question');
expect(option?.localeKey).toBe(
'smart-snippet-feedback-reason-was-not-a-question'
);
});

it('should include "other" option', () => {
const option = feedbackOptions.find(
(opt) => opt.correspondingAnswer === 'other'
);

expect(option).toBeDefined();
expect(option?.id).toBe('other');
expect(option?.localeKey).toBe('smart-snippet-feedback-reason-other');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type {SmartSnippetFeedback} from '@coveo/headless';

export const feedbackOptions: {
id: string;
localeKey: string;
correspondingAnswer: SmartSnippetFeedback | 'other';
}[] = [
{
id: 'does-not-answer',
localeKey: 'smart-snippet-feedback-reason-does-not-answer',
correspondingAnswer: 'does_not_answer',
},
{
id: 'partially-answers',
localeKey: 'smart-snippet-feedback-reason-partially-answers',
correspondingAnswer: 'partially_answers',
},
{
id: 'was-not-a-question',
localeKey: 'smart-snippet-feedback-reason-was-not-a-question',
correspondingAnswer: 'was_not_a_question',
},
{
id: 'other',
localeKey: 'smart-snippet-feedback-reason-other',
correspondingAnswer: 'other',
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {html} from 'lit';
import {describe, expect, it, vi} from 'vitest';
import {renderFunctionFixture} from '@/vitest-utils/testing-helpers/fixture';
import {renderModalBody} from './modal-body';

describe('#renderModalBody', () => {
const formId = 'test-form-id';
const onSubmit = vi.fn((e: Event) => e.preventDefault());

const renderComponent = async () => {
return await renderFunctionFixture(
html`${renderModalBody({
props: {formId, onSubmit},
})(html`<div>Child content</div>`)}`
);
};

it('should render with valid props', async () => {
const element = await renderComponent();
expect(element).toBeDefined();
});

it('should render form element with correct attributes', async () => {
const element = await renderComponent();
const form = element.querySelector('form');

expect(form).not.toBeNull();
expect(form?.getAttribute('id')).toBe(formId);
expect(form?.getAttribute('slot')).toBe('body');
expect(form?.part).toContain('form');
});

it('should render children inside form', async () => {
const element = await renderComponent();
const form = element.querySelector('form');

expect(form).toHaveTextContent('Child content');
});

it('should call onSubmit when form is submitted', async () => {
const element = await renderComponent();
const form = element.querySelector('form') as HTMLFormElement;

form.dispatchEvent(new Event('submit'));

expect(onSubmit).toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {html} from 'lit';
import type {FunctionalComponentWithChildren} from '@/src/utils/functional-component-utils';

export interface ModalBodyProps {
formId: string;
onSubmit: (e: Event) => void;
}

export const renderModalBody: FunctionalComponentWithChildren<ModalBodyProps> =
({props: {formId, onSubmit}}) =>
(children) =>
html`<form
part="form"
id=${formId}
slot="body"
@submit=${onSubmit}
class="flex flex-col gap-8"
>
${children}
</form>`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import type {SmartSnippetFeedback} from '@coveo/headless';
import type {i18n} from 'i18next';
import {html} from 'lit';
import {beforeAll, describe, expect, it, vi} from 'vitest';
import {renderFunctionFixture} from '@/vitest-utils/testing-helpers/fixture';
import {createTestI18n} from '@/vitest-utils/testing-helpers/i18n-utils';
import {renderModalDetails} from './modal-details';

describe('#renderModalDetails', () => {
let i18n: i18n;

beforeAll(async () => {
i18n = await createTestI18n();
});

const renderComponent = async (
currentAnswer?: SmartSnippetFeedback | 'other',
detailsInputRef?: (el: Element | undefined) => void
) => {
return await renderFunctionFixture(
html`${renderModalDetails({
props: {currentAnswer, i18n, detailsInputRef},
})}`
);
};

it('should render with valid props when currentAnswer is "other"', async () => {
const element = await renderComponent('other');
expect(element).toBeDefined();
});

it('should not render when currentAnswer is not "other"', async () => {
const element = await renderComponent('does_not_answer');
const fieldset = element.querySelector('fieldset');

expect(fieldset).toBeNull();
});

it('should not render when currentAnswer is undefined', async () => {
const element = await renderComponent();
const fieldset = element.querySelector('fieldset');

expect(fieldset).toBeNull();
});

it('should render fieldset when currentAnswer is "other"', async () => {
const element = await renderComponent('other');
const fieldset = element.querySelector('fieldset');

expect(fieldset).not.toBeNull();
expect(fieldset?.tagName).toBe('FIELDSET');
});

it('should render legend with correct attributes and classes', async () => {
const element = await renderComponent('other');
const legend = element.querySelector('legend');

expect(legend).not.toBeNull();
expect(legend?.part).toContain('details-title');
expect(legend).toHaveClass('text-on-background', 'text-lg', 'font-bold');
});

it('should render translated legend text', async () => {
const element = await renderComponent('other');
const legend = element.querySelector('legend');

expect(legend).toHaveTextContent(i18n.t('details'));
});

it('should render textarea with correct attributes', async () => {
const element = await renderComponent('other');
const textarea = element.querySelector('textarea');

expect(textarea).not.toBeNull();
expect(textarea?.getAttribute('name')).toBe('answer-details');
expect(textarea?.part).toContain('details-input');
expect(textarea?.getAttribute('rows')).toBe('4');
expect(textarea?.hasAttribute('required')).toBe(true);
});

it('should apply ref when detailsInputRef is provided', async () => {
const detailsInputRef = vi.fn();

const element = await renderComponent('other', detailsInputRef);
const textarea = element.querySelector('textarea');

expect(detailsInputRef).toHaveBeenCalledWith(textarea);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type {SmartSnippetFeedback} from '@coveo/headless';
import type {i18n} from 'i18next';
import {html} from 'lit';
import {type RefOrCallback, ref} from 'lit/directives/ref.js';
import {when} from 'lit/directives/when.js';
import type {FunctionalComponent} from '@/src/utils/functional-component-utils';

export interface ModalDetailsProps {
currentAnswer: SmartSnippetFeedback | 'other';
i18n: i18n;
detailsInputRef: RefOrCallback;
}

export const renderModalDetails: FunctionalComponent<ModalDetailsProps> = ({
props: {currentAnswer, i18n, detailsInputRef},
}) =>
when(
currentAnswer === 'other',
() =>
html`<fieldset>
<legend
part="details-title"
class="text-on-background text-lg font-bold"
>
${i18n.t('details')}
</legend>
<textarea
part="details-input"
name="answer-details"
${ref(detailsInputRef)}
class="border-neutral mt-2 w-full resize-none rounded border p-2 text-base leading-5"
rows=${4}
required
></textarea>
</fieldset>`
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type {i18n} from 'i18next';
import {html} from 'lit';
import {beforeAll, describe, expect, it, vi} from 'vitest';
import {renderFunctionFixture} from '@/vitest-utils/testing-helpers/fixture';
import {createTestI18n} from '@/vitest-utils/testing-helpers/i18n-utils';
import {renderModalFooter} from './modal-footer';

vi.mock('@/src/utils/ripple-utils', {spy: true});

describe('#renderModalFooter', () => {
let i18n: i18n;

beforeAll(async () => {
i18n = await createTestI18n();
});

const formId = 'test-form-id';
const onClick = vi.fn();

const renderComponent = async () => {
return await renderFunctionFixture(
html`${renderModalFooter({
props: {formId, i18n, onClick},
})}`
);
};

it('should render with valid props', async () => {
const element = await renderComponent();
expect(element).toBeDefined();
});

it('should render container div with correct attributes', async () => {
const element = await renderComponent();
const container = element.querySelector('[part="buttons"]');

expect(container).not.toBeNull();
expect(container?.tagName).toBe('DIV');
expect(container?.getAttribute('slot')).toBe('footer');
expect(container).toHaveClass('flex', 'justify-end', 'gap-2');
});

it('should render cancel button with correct part', async () => {
const element = await renderComponent();
const cancelButton = element.querySelector('[part="cancel-button"]');

expect(cancelButton).not.toBeNull();
expect(cancelButton?.tagName).toBe('BUTTON');
});

it('should render cancel button with translated text', async () => {
const element = await renderComponent();
const cancelButton = element.querySelector('[part="cancel-button"]');

expect(cancelButton).toHaveTextContent(i18n.t('cancel'));
});

it('should call onClick when cancel button is clicked', async () => {
const element = await renderComponent();
const cancelButton = element.querySelector(
'[part="cancel-button"]'
) as HTMLButtonElement;

cancelButton.click();

expect(onClick).toHaveBeenCalled();
});

it('should render submit button with correct part and attributes', async () => {
const element = await renderComponent();
const submitButton = element.querySelector('[part="submit-button"]');

expect(submitButton).not.toBeNull();
expect(submitButton?.tagName).toBe('BUTTON');
expect(submitButton?.getAttribute('type')).toBe('submit');
expect(submitButton?.getAttribute('form')).toBe(formId);
});

it('should render submit button with translated text', async () => {
const element = await renderComponent();
const submitButton = element.querySelector('[part="submit-button"]');

expect(submitButton).toHaveTextContent(i18n.t('feedback-send'));
});
});
Loading
Loading