Skip to content
Open
49 changes: 49 additions & 0 deletions __tests__/autocompleteBeforeReady.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { act, renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { defaultSlateConverter } from '../src/components/smartEditor/helpers';
import { useLspAutocomplete } from '../src/components/smartEditor/useLspAutocomplete';
import { ReactEditor } from 'slate-react';

/** The editor as Slate holds it after a key: its text, the caret at the end, and the edit's operations. */
const editorWith = (text: string, operations: { type: string }[] = [{ type: 'insert_text' }]) =>
Expand Down Expand Up @@ -220,4 +221,52 @@ describe('the template picker when the window is resized', () => {
button.remove();
}
});

/* A click in the text opens the picker at the caret, with nothing it was opened from: it was left at the point of the
window it opened at while the window's width moved the text away (qorus-ide #301, qlip #370). */
it('opened at the caret, stays where it was opened in the text as the text moves', () => {
const { hook } = setup(true);
const text = document.createElement('div');
document.body.appendChild(text);
const drawn = vi.spyOn(ReactEditor, 'toDOMNode').mockReturnValue(text);
try {
const measure = vi.spyOn(text, 'getBoundingClientRect').mockReturnValue(boxAt(320, 58) as DOMRect);
// the caret 40 px into the text, on its first line
act(() =>
hook.result.current.openBrowse(editorWith('hello') as never, {
rect: { left: 360, bottom: 40 },
caret: { path: [0, 0], offset: 2 },
} as never)
);
expect(hook.result.current.position).toEqual({ left: 360, top: 40 });

measure.mockReturnValue(boxAt(900, 58) as DOMRect);
resized();
expect(hook.result.current.position).toEqual({ left: 940, top: 40 });
expect(hook.result.current.browse).toBeTruthy();
} finally {
drawn.mockRestore();
text.remove();
}
});

it('opened from an element, stays as far from it as when it opened', () => {
const { hook } = setup(true);
const chip = document.createElement('span');
document.body.appendChild(chip);
try {
const measure = vi.spyOn(chip, 'getBoundingClientRect').mockReturnValue(boxAt(100, 30) as DOMRect);
act(() =>
hook.result.current.openBrowse(editorWith('') as never, {
rect: { left: 100, bottom: 34 },
anchor: chip,
} as never)
);
measure.mockReturnValue(boxAt(600, 130) as DOMRect);
resized();
expect(hook.result.current.position).toEqual({ left: 600, top: 134 });
} finally {
chip.remove();
}
});
});
81 changes: 81 additions & 0 deletions __tests__/formEngineExpandOptionsRequest.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright 2026 Qore Technologies, s.r.o.
/* A host opens a row of a form that is already drawn, without drawing the form again (`expandOptionsRequest`). */
import { ReqoreUIProvider } from '@qoretechnologies/reqore';
import { act, fireEvent, render, waitFor } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { FormEngine } from '../src/components/form/engine/FormEngine';
import { FetchContext } from '../src/contexts/FetchContext';
import { emptyFetchContext } from './support/fetchContext';

const OPTIONS = {
first: { type: 'string', display_name: 'First' },
second: { type: 'string', display_name: 'Second' },
};
const VALUE = { first: { type: 'string', value: 'a' }, second: { type: 'string', value: 'b' } };

const draw = (
props: Record<string, unknown> = {},
options: unknown = OPTIONS,
value: unknown = VALUE
) => (
<ReqoreUIProvider>
<FetchContext.Provider value={emptyFetchContext()}>
<FormEngine
compact
name='request'
value={value as never}
options={options as never}
onChange={vi.fn()}
{...props}
/>
</FetchContext.Provider>
</ReqoreUIProvider>
);

const open = (name: string) =>
!!document.querySelector(`[data-field="${name}"]`)?.querySelector('.readfirst-row-editing') ||
!!document.querySelector(`[data-field="${name}"].readfirst-row-editing`);

describe('a row a host asks to open', () => {
it('opens in the form as it is drawn, and what the reader opened stays open', async () => {
const { rerender } = render(draw());
await waitFor(() => expect(document.querySelector('[data-field="first"]')).toBeTruthy());
act(() => {
fireEvent.click(document.querySelector('[data-field="first"]')!);
});
await waitFor(() => expect(open('first')).toBe(true));
rerender(draw({ expandOptionsRequest: { options: ['second'], key: 1 } }));
await waitFor(() => expect(open('second')).toBe(true));
// drawn again, the row the reader opened would be closed
expect(open('first')).toBe(true);
});

it('is not opened again for a key it has answered, and is for a new one', async () => {
const { rerender } = render(draw({ expandOptionsRequest: { options: ['second'], key: 1 } }));
await waitFor(() => expect(open('second')).toBe(true));
const done = document
.querySelector('[data-field="second"]')
?.querySelector<HTMLElement>('[aria-label="Done"], [aria-label="Close field"]');
expect(done, 'the row’s close').toBeTruthy();
act(() => {
fireEvent.click(done!);
});
await waitFor(() => expect(open('second')).toBe(false));
rerender(draw({ expandOptionsRequest: { options: ['second'], key: 1 } }));
expect(open('second')).toBe(false);
rerender(draw({ expandOptionsRequest: { options: ['second'], key: 2 } }));
await waitFor(() => expect(open('second')).toBe(true));
});

it('waits for a row the form does not have yet', async () => {
const request = { options: ['second'], key: 'late' };
const firstOnly = { first: VALUE.first };
const { rerender } = render(
draw({ expandOptionsRequest: request }, { first: OPTIONS.first }, firstOnly)
);
await waitFor(() => expect(document.querySelector('[data-field="first"]')).toBeTruthy());
expect(open('second')).toBe(false);
rerender(draw({ expandOptionsRequest: request }));
await waitFor(() => expect(open('second')).toBe(true));
});
});
185 changes: 185 additions & 0 deletions __tests__/reqraftLabels.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright 2026 Qore Technologies, s.r.o.
/* A host's words reach what Reqraft draws: merged over the English defaults, and again when its language changes. */
import { render, renderHook, screen, act } from '@testing-library/react';
import { ReqoreUIProvider } from '@qoretechnologies/reqore';
import { afterEach, describe, expect, it } from 'vitest';
import {
CONTEXT_DATA_PAYLOAD,
QOG_STATE_DATA_PAYLOAD,
} from '../src/components/form/fields/template/__fixtures__/qogTemplatePayloads';
import { buildTemplates } from '../src/helpers/templates';
import {
DEFAULT_REQRAFT_LABELS,
reqraftLabels,
setReqraftLabels,
useReqraftLabels,
} from '../src/helpers/reqraftLabels';
import { templateNodeKindLabel, templateTree, referenceNamesNothingMessage } from '../src/helpers/templateTree';
import { useTemplates, seedTemplatesCache, TemplatesCache } from '../src/hooks/useTemplates';
import { hiddenFieldNoteText } from '../src/components/form/engine/HiddenFieldsNote';
import { formatOptionValue } from '../src/components/form/engine/readFirst';
import { formatCodeLines } from '../src/components/codeSize';
import { useWholeSays } from '../src/components/form/fields/template/TemplatePicker';
import { browseAllTemplatesItem } from '../src/components/smartEditor/templateCompletions';
import { dpqlEmptyCompletionMessage } from '../src/components/dpqlEditor/dpqlCompletions';
import { toDropdownItems } from '../src/components/smartEditor/useLspAutocomplete';
import { describeDependencies } from '../src/components/form/engine/OptionFieldMessages';

const payload = { ...CONTEXT_DATA_PAYLOAD, ...QOG_STATE_DATA_PAYLOAD } as never;

/** Every description in a list of templates, however deep. */
const allDescriptions = (items: unknown[] = []): string =>
(items as { description?: unknown; items?: unknown[] }[])
.map((item) => `${item.description ?? ''} ${allDescriptions(item.items)}`)
.join(' ');

afterEach(() => {
setReqraftLabels();
TemplatesCache.clear();
});

describe('the labels', () => {
it('are the English defaults until a host gives others', () => {
expect(reqraftLabels()).toEqual(DEFAULT_REQRAFT_LABELS);
setReqraftLabels({ formEngine: { save: 'Speichern' } });
expect(reqraftLabels().formEngine.save).toBe('Speichern');
// the rest stay English, here and in the other parts
expect(reqraftLabels().formEngine.discard).toBe('Discard');
expect(reqraftLabels().editor).toEqual(DEFAULT_REQRAFT_LABELS.editor);
});

it('merge a map entry by entry', () => {
setReqraftLabels({ templateTree: { kinds: { field: 'Feld' } } });
expect(templateNodeKindLabel('field')).toBe('Feld');
expect(templateNodeKindLabel('variable')).toBe('Variable');
expect(templateNodeKindLabel('group')).toBeUndefined();
});

it('skip a word given as undefined', () => {
setReqraftLabels({ formEngine: { save: undefined } });
expect(reqraftLabels().formEngine.save).toBe('Save');
});

it('replace what the last call gave, and say so to a component', () => {
const { result } = renderHook(() => useReqraftLabels().formEngine.save);
expect(result.current).toBe('Save');
act(() => setReqraftLabels({ formEngine: { save: 'Speichern' } }));
expect(result.current).toBe('Speichern');
act(() => setReqraftLabels({ formEngine: { discard: 'Verwerfen' } }));
expect(result.current).toBe('Save');
});

it('let a component go without leaving a listener', () => {
const { result, unmount } = renderHook(() => useReqraftLabels());
const seen = result.current;
unmount();
act(() => setReqraftLabels({ formEngine: { save: 'Speichern' } }));
expect(result.current).toBe(seen);
});
});

describe('a list of templates', () => {
const description = (templates?: ReturnType<typeof buildTemplates>) =>
allDescriptions(templates?.items);

it('reads in the host’s words; those it is given itself come first', () => {
setReqraftLabels({ templateList: { exampleValue: (value) => `Beispiel: ${value}` } });
expect(description(buildTemplates(payload))).toContain('Beispiel:');
expect(description(buildTemplates(payload, { exampleValue: (value) => `Ex ${value}` }))).toContain('Ex ');
});

it('kept between forms is read again when the language changes, without asking the server', () => {
seedTemplatesCache('generic', payload);
const { result } = renderHook(() => useTemplates(true, undefined, 'generic'));
expect(description(result.current.value)).toContain('Example value:');
act(() => setReqraftLabels({ templateList: { exampleValue: (value) => `Beispiel: ${value}` } }));
expect(description(result.current.value)).toContain('Beispiel:');
});
});

describe('the template tree', () => {
const fields = { order: { type: 'string' } } as never;

it('names its groups in the host’s words, also after a language change', () => {
const named = () => templateTree(buildTemplates(payload), { fields }).map((node) => node.label);
expect(named()).toContain('Record fields');
setReqraftLabels({ templateTree: { recordFields: 'Datensatzfelder' } });
expect(named()).toContain('Datensatzfelder');
setReqraftLabels();
expect(named()).toContain('Record fields');
});

it('says what a reference that names nothing is', () => {
setReqraftLabels({ templateTree: { referenceNamesNothing: (written) => `Nichts: ${written}` } });
expect(referenceNamesNothingMessage('$x:')).toBe('Nichts: $x:');
});

it('says what the picker’s Use takes', () => {
setReqraftLabels({ templateBrowser: { useWhole: (label) => `Ganz: ${label}` } });
expect(useWholeSays('Order')).toBe('Ganz: Order');
});
});

describe('the editor', () => {
it('names a completion’s kind in the host’s words', () => {
const items = () => toDropdownItems([{ label: 'a', kind: 3 } as never]);
expect(items()[0].kindLabel).toBe('Function');
setReqraftLabels({ editor: { completionKinds: { 3: 'Funktion' } } });
expect(items()[0].kindLabel).toBe('Funktion');
});

it('offers the way to all templates in the host’s words', () => {
setReqraftLabels({ editor: { browseAllTemplates: 'Alle Vorlagen' } });
expect(browseAllTemplatesItem().label).toBe('Alle Vorlagen');
});

it('says why nothing is listed in the host’s words', () => {
setReqraftLabels({ editor: { noTemplates: 'Keine Vorlagen' } });
expect(dpqlEmptyCompletionMessage('$', 1)).toBe('Keine Vorlagen');
});
});

describe('a form', () => {
it('says what a value reads as in the host’s words', () => {
const format = () => formatOptionValue({ type: 'bool', value: true } as never, { type: 'bool' } as never);
expect(format()).toBe('Yes');
setReqraftLabels({ formEngine: { yes: 'Ja' } });
expect(format()).toBe('Ja');
});

it('says why a field is hidden, the English as it was', () => {
const note = { label: 'A', conditions: 'b is set', invalidReason: 'too long.' } as never;
expect(hiddenFieldNoteText(note)).toBe(
'"A" is hidden until it applies (b is set). Its value is kept, but it is not valid: too long.'
);
setReqraftLabels({ formEngine: { hiddenUntilItApplies: (label) => `${label} ist versteckt` } });
expect(hiddenFieldNoteText(note)).toBe('A ist versteckt');
});

it('counts in the host’s words', () => {
expect(formatCodeLines(1)).toBe('1 line');
expect(formatCodeLines(2)).toBe('2 lines');
setReqraftLabels({ formEngine: { lineCount: (count) => `${count} Zeilen` } });
expect(formatCodeLines(2)).toBe('2 Zeilen');
});

it('joins alternative dependencies in the host’s words', () => {
const schema = { a: { display_name: 'A' }, b: { display_name: 'B' } } as never;
expect(describeDependencies([['a', 'b']], schema)).toBe('("A" or "B")');
setReqraftLabels({ formEngine: { orJoin: (alternatives) => alternatives.join(' oder ') } });
expect(describeDependencies([['a', 'b']], schema)).toBe('"A" oder "B"');
});

it('draws its toolbar words again when the language changes', () => {
const Words = () => <span>{useReqraftLabels().formEngine.filterFields}</span>;
render(
<ReqoreUIProvider>
<Words />
</ReqoreUIProvider>
);
expect(screen.getByText('Filter fields...')).toBeTruthy();
act(() => setReqraftLabels({ formEngine: { filterFields: 'Felder filtern' } }));
expect(screen.getByText('Felder filtern')).toBeTruthy();
});
});

45 changes: 45 additions & 0 deletions __tests__/reqraftLabelsDrawn.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2026 Qore Technologies, s.r.o.
/* What is on the page says a host's words, and says others when its language changes, with nothing else redrawing it. */
import { ReqoreUIProvider } from '@qoretechnologies/reqore';
import { act, render, waitFor } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { FormEngine } from '../src/components/form/engine/FormEngine';
import { FetchContext } from '../src/contexts/FetchContext';
import { setReqraftLabels } from '../src/helpers/reqraftLabels';
import { emptyFetchContext } from './support/fetchContext';

afterEach(() => {
setReqraftLabels();
});

const form = () =>
render(
<ReqoreUIProvider>
<FetchContext.Provider value={emptyFetchContext()}>
<FormEngine
compact
name='words'
value={{ choice: { type: 'bool', value: true } } as never}
options={{ choice: { type: 'bool', display_name: 'Choice' } } as never}
onChange={vi.fn()}
/>
</FetchContext.Provider>
</ReqoreUIProvider>
);

describe('a form', () => {
it('says the English it always did when a host gives nothing', async () => {
form();
await waitFor(() => expect(document.body.textContent).toContain('Yes'));
});

it('says a host’s words, and again when the language changes', async () => {
setReqraftLabels({ formEngine: { yes: 'Ja' } });
form();
await waitFor(() => expect(document.body.textContent).toContain('Ja'));

act(() => setReqraftLabels({ formEngine: { yes: 'Oui' } }));
await waitFor(() => expect(document.body.textContent).toContain('Oui'));
expect(document.body.textContent).not.toContain('Ja');
});
});
Loading
Loading