Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0037c49
feat(form-engine): an opt-in calm appearance for the compact form — 0…
Foxhoundn Oct 19, 2026
41b108d
chore(version): 0.10.63 — one patch bump, as the PR gate requires
Foxhoundn Oct 19, 2026
2aae872
fix(form-engine): a calm form's informative notes are not blue
Foxhoundn Oct 19, 2026
570be90
fix(supportTicket): the ticket rail's labels take the theme's case
Foxhoundn Oct 19, 2026
15234a3
feat(form-engine): a new form is not already wrong
Foxhoundn Oct 22, 2026
c883ec4
Merge remote-tracking branch 'origin/develop' into feature/144_form-e…
Foxhoundn Oct 22, 2026
7303dfe
fix(form-engine): an open field on a new form is not already wrong ei…
Foxhoundn Oct 22, 2026
77e07b7
feat(form-engine): tell the host what is left, and answer a blocked s…
Foxhoundn Oct 22, 2026
d2fe106
fix(form-engine): the attention filter never hides the row being type…
Foxhoundn Oct 22, 2026
857a198
fix(form-engine): a field that opens as a card takes the caret, and "…
Foxhoundn Oct 22, 2026
fa26fd9
fix(form-engine): a card hands the caret only to where text goes, and…
Foxhoundn Oct 22, 2026
15a8784
fix(form-engine): a late text box takes the caret from the editor's c…
Foxhoundn Oct 22, 2026
c151336
fix(form-engine): the caret counts as handed over only once it has la…
Foxhoundn Oct 22, 2026
09249a6
fix(FormEngine): a box counts what its fields hold now, also while on…
Foxhoundn Oct 26, 2026
4bd3ad7
fix(TicketReplyBox): a ticket reply does not wear Qonsole's look
Foxhoundn Oct 27, 2026
ec53754
Merge remote-tracking branch 'origin/develop' into feature/144_form-e…
Foxhoundn Oct 27, 2026
32331ea
chore(release): bump version to 0.10.66
Foxhoundn Oct 27, 2026
2d1b4c8
fix(support): the ticket thread and its references do not wear Qonsol…
Foxhoundn Oct 27, 2026
87cc40b
fix(support): an attachment whose bytes are not a picture falls back …
Foxhoundn Oct 27, 2026
ac8f9bf
fix(form): a text or number field honours debounceMs, so a form submi…
Foxhoundn Oct 27, 2026
5f8e685
fix(form): an expression preview writes a rich-text argument as the s…
Foxhoundn Oct 27, 2026
b17724d
fix(form): a choice picker reads an allowed value however its entry i…
Foxhoundn Oct 27, 2026
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
86 changes: 86 additions & 0 deletions __tests__/allowedValueSpellings.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { ReqoreUIProvider } from '@qoretechnologies/reqore';
import { fireEvent, render, screen } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';
import { FieldAllowedValues } from '../src/components/form/fields/allowed-values/AllowedValues';
import { allowedValueOf, findAllowedValueOption } from '../src/helpers/allowedValues';

/**
* A choice is the same choice however its entry is written.
*
* An `allowed_values` entry comes as an envelope (`{value: {type, value}}`), a
* bare value (`{value: 'x'}`) or a name alone (`{name: 'x'}`) — the app
* catalogue sends the bare form, a `context=ui` answer the envelope. The row
* that names a stored choice read all three; the pickers that write one read
* only the envelope. On a bare list nothing showed as selected, and clicking a
* choice stored `undefined`: picking a pipeline processor's class cleared it.
*/
describe('the value a choice stands for', () => {
it('reads the envelope, the bare value and the name', () => {
expect(allowedValueOf({ value: { type: 'string', value: 'a' } })).toBe('a');
expect(allowedValueOf({ value: 'b' })).toBe('b');
expect(allowedValueOf({ name: 'c' })).toBe('c');
// A falsy bare value is still the value, not a reason to fall back to the name.
expect(allowedValueOf({ name: 'zero', value: 0 })).toBe(0);
expect(allowedValueOf({ value: false })).toBe(false);
});

it('agrees with the lookup that names the stored choice', () => {
for (const entry of [
{ name: 'x', value: { type: 'string', value: 'v' } },
{ name: 'x', value: 'v' },
{ name: 'v' },
]) {
const schema = { type: 'string', allowed_values: [entry] } as never;
expect(findAllowedValueOption(allowedValueOf(entry), schema)).toBe(entry);
}
});
});

describe('a short list written with bare values', () => {
const items = [
{ name: 'ProcessorTypeTest', display_name: 'Processor Type Test', value: 'ProcessorTypeTest' },
{
name: 'qogcompatibilitytest',
display_name: 'Qog Compatibility Test',
value: 'qogcompatibilitytest',
},
];

it('stores the choice that was clicked', () => {
const onChange = vi.fn();
render(
<ReqoreUIProvider>
<FieldAllowedValues
type='string'
name='processor'
value='ProcessorTypeTest'
onChange={onChange}
items={items as never}
/>
</ReqoreUIProvider>
);

fireEvent.click(screen.getByText('Qog Compatibility Test').closest('.reqore-checkbox')!);
expect(onChange).toHaveBeenCalledWith('processor', 'qogcompatibilitytest');
});

it('knows the stored value is the selected choice', () => {
const onChange = vi.fn();
render(
<ReqoreUIProvider>
<FieldAllowedValues
type='string'
name='processor'
value='ProcessorTypeTest'
onChange={onChange}
items={items as never}
/>
</ReqoreUIProvider>
);

// Clicking the selected choice unselects it, which is only possible if the
// picker recognised it as the selected one.
fireEvent.click(screen.getByText('Processor Type Test').closest('.reqore-checkbox')!);
expect(onChange).toHaveBeenCalledWith('processor', undefined);
});
});
83 changes: 83 additions & 0 deletions __tests__/compactRowCaretFindsTheControl.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,89 @@ describe('the caret an opened row takes', () => {
expect((document.activeElement as HTMLElement).textContent).toContain('Choose a connection');
});

/* A code editor draws its chrome first (a fullscreen toggle) and its edit area once
it has loaded. The caret may rest on the chrome for that moment, but the edit area
takes it when it arrives: otherwise the next space typed presses the toggle. */
const LateCodeEditor = ({ onMount }: { onMount: (mount: () => void) => void }) => {
const [ready, setReady] = React.useState(false);
React.useEffect(() => onMount(() => setReady(true)), []);
return (
<div>
<button type='button'>Fullscreen</button>
{ready ? <div role='textbox' tabIndex={0} aria-label='Editor content' /> : null}
</div>
);
};
const codeField = {
source: { type: 'string', ui_type: 'late-code', display_name: 'Source', required: true },
};

it('moves from an editor\'s chrome to its text box when the text box arrives late', async () => {
let mount: () => void = () => undefined;
renderForm(codeField, {}, {
componentOverrides: {
'late-code': () => <LateCodeEditor onMount={(fn) => (mount = fn)} />,
},
});

await waitFor(() => expect(focusedField()).toBe('source'));
expect(document.activeElement?.textContent).toBe('Fullscreen');

mount();

await waitFor(() =>
expect(document.activeElement?.getAttribute('aria-label')).toBe('Editor content')
);
});

/* Monaco inserts its edit area before it can take focus, and focus() on it then does
nothing. The caret counts as handed over only once it has actually landed. */
it('keeps trying until the text box can actually take the caret', async () => {
let makeFocusable: () => void = () => undefined;
const SlowEditArea = () => {
const [focusable, setFocusable] = React.useState(false);
React.useEffect(() => {
makeFocusable = () => setFocusable(true);
}, []);
return (
<div>
<button type='button'>Fullscreen</button>
<div role='textbox' aria-label='Editor content' {...(focusable ? { tabIndex: 0 } : {})} />
</div>
);
};
renderForm(codeField, {}, { componentOverrides: { 'late-code': SlowEditArea } });

// The text box is there but cannot take focus yet: the row waits for it rather
// than settling on the toolbar beside it.
await waitFor(() => expect(document.querySelector('.readfirst-row-editing')).toBeTruthy());
expect(document.activeElement?.textContent).not.toBe('Fullscreen');

makeFocusable();

await waitFor(() =>
expect(document.activeElement?.getAttribute('aria-label')).toBe('Editor content')
);
});

it('leaves the caret where the reader put it, even when the text box arrives later', async () => {
let mount: () => void = () => undefined;
const { container } = renderForm(codeField, {}, {
componentOverrides: {
'late-code': () => <LateCodeEditor onMount={(fn) => (mount = fn)} />,
},
});
await waitFor(() => expect(focusedField()).toBe('source'));

const elsewhere = document.createElement('input');
container.appendChild(elsewhere);
elsewhere.focus();
mount();

await waitFor(() => expect(document.querySelector('[aria-label="Editor content"]')).toBeTruthy());
expect(document.activeElement).toBe(elsewhere);
});

it('leaves a caret the reader moved elsewhere alone', async () => {
// A late editor plus a reader who did not wait: the row must not yank the
// caret out of the control they moved into themselves.
Expand Down
48 changes: 48 additions & 0 deletions __tests__/expressionRichTextArgument.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it } from 'vitest';

import { renderExpressionToText } from '../src/components/form/expressions/renderExpressionToText';
import { IExpression, IExpressionValue } from '../src/components/form/expressions/types';

/**
* A rich-text argument reads as the string it is.
*
* The editor stores rich text as its document — paragraphs of text and template
* tags — and the offline renderer printed that object as JSON. A Qog's Message
* Content row read `+([{"type":"paragraph","children":[…]}], …)`: the editor's
* node tree, which is no value anyone wrote and not what the server renders.
*/
const richtext = (...children: unknown[]): IExpression =>
({ type: 'richtext', value: [{ type: 'paragraph', children }] }) as IExpression;
const lit = (value: unknown): IExpression => ({ value }) as IExpression;

describe('renderExpressionToText with a rich-text argument', () => {
it('writes the rich text as a string literal', () => {
const value = {
exp: '+',
args: [richtext({ text: 'This Qog was run at ' }), lit(' by me')],
} as IExpressionValue;

expect(renderExpressionToText(value)).toBe('"This Qog was run at " + " by me"');
});

it('writes a template tag inside it as its reference', () => {
const value = {
exp: '+',
args: [
richtext(
{ text: 'Day ends at ' },
{ type: 'tag', label: 'Day End', value: '$timestamp:next_midnight', children: [{ text: '' }] }
),
lit('!'),
],
} as IExpressionValue;

expect(renderExpressionToText(value)).toBe('"Day ends at $timestamp:next_midnight" + "!"');
});

it('still writes any other object as it is', () => {
const value = { exp: '+', args: [lit({ a: 1 }), lit(2)] } as IExpressionValue;

expect(renderExpressionToText(value)).toBe('{"a":1} + 2');
});
});
4 changes: 3 additions & 1 deletion __tests__/formEngineCompletionMeter.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ describe('the completion header', () => {
expect(text).not.toContain('Draft');
expect(text).not.toContain('Ready');
expect(text).toContain('0/2 set');
expect(text).toContain('2 need attention');
// Nobody has been to either field yet, so they are still to fill in, not faults.
expect(text).toContain('2 to fill in');
expect(text).not.toContain('need attention');
});

it('still reads Ready at 100% when nothing is flagged', async () => {
Expand Down
Loading
Loading