Skip to content
Merged
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
61 changes: 61 additions & 0 deletions __tests__/formFieldsSkeletonNamesItsWait.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2026 Qore Technologies, s.r.o.
// A field skeleton always says what it is waiting for.
//
// `data-wait` is how a page — and a story, before its visual capture — asks
// whether a form has finished loading. FormFieldsSkeleton rendered it only when
// its caller named a reason, and the nested waits (a field resolving its named
// arg schema, a hash resolving its sub-schema) named none, so they drew a
// placeholder nothing could wait for: qorus-ide's Qlip build 279 captured the AI
// guardrail's Rules row on one.
import { ReqoreUIProvider } from '@qoretechnologies/reqore';
import { render, waitFor } from '@testing-library/react';
import { describe, expect, it, vi } from 'vitest';

vi.mock('../src/utils/fetch', async (importOriginal) => {
const original = await importOriginal<typeof import('../src/utils/fetch')>();
// A schema request the server has not answered yet.
return { ...original, query: vi.fn(() => new Promise(() => {})) };
});

import { FormFieldsSkeleton } from '../src/components/form/engine/FormFieldsSkeleton';
import { AutoFormField } from '../src/components/form/fields/auto/AutoFormField';
import { FetchContext } from '../src/contexts/FetchContext';
import { emptyFetchContext } from './support/fetchContext';

const wrap = (node: React.ReactNode) => (
<ReqoreUIProvider>
<FetchContext.Provider value={emptyFetchContext()}>{node}</FetchContext.Provider>
</ReqoreUIProvider>
);

describe('FormFieldsSkeleton', () => {
it('names the wait it stands for', () => {
const { container } = render(wrap(<FormFieldsSkeleton rows={1} reason='templates' />));

expect(container.querySelector('[data-wait]')?.getAttribute('data-wait')).toBe('templates');
});

it('is still marked when its caller names no wait', () => {
const { container } = render(wrap(<FormFieldsSkeleton rows={1} />));

expect(container.querySelector('[data-wait]')?.getAttribute('data-wait')).toBe('loading');
});
});

describe('a field resolving its named arg schema', () => {
it('shows a placeholder that says it waits for the schema', async () => {
const { container } = render(
wrap(
<AutoFormField
name='rules'
type='list'
defaultType='list'
arg_schema='ai-guardrail-rules'
onChange={vi.fn()}
/>
)
);

await waitFor(() => expect(container.querySelector('[data-wait="arg-schema"]')).not.toBeNull());
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@qoretechnologies/reqraft",
"version": "0.10.60",
"version": "0.10.61",
"description": "ReQraft is a collection of React components and hooks that are used across Qore Technologies' products made using the ReQore component library from Qore.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
7 changes: 6 additions & 1 deletion src/components/form/engine/FormFieldsSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ export interface IFormFieldsSkeletonProps {
* in the same commit and neither of the two shared waits (`types`, 140ms;
* `templates`, 1500ms) explained it. Naming the reason in the DOM turns that
* from a guess into a reading.
*
* Always rendered, `loading` when the caller names none: `data-wait` is also
* how a page asks whether it has finished loading, so a placeholder without
* it is one nothing can wait for. The nested waits named none, and a story
* handed a skeleton to its visual capture (qorus-ide Qlip build 279).
*/
reason?: string;
}
Expand All @@ -50,7 +55,7 @@ export const FormFieldsSkeleton = ({
rows = 6,
fill,
className,
reason,
reason = 'loading',
}: IFormFieldsSkeletonProps) => (
<ReqoreControlGroup
className={className}
Expand Down
2 changes: 1 addition & 1 deletion src/components/form/fields/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ export const FormField = <T extends TFormFieldType>({
case 'hash': {
if (argSchemaLoading) {
// A hash renders a nested FORM, so it waits in the shape of one.
return <FormFieldsSkeleton rows={2} />;
return <FormFieldsSkeleton rows={2} reason='arg-schema' />;
}
if (finalArgSchema) {
return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/form/fields/auto/AutoFormField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ function AutoField<T = any>({
/* One ROW, because this stands in for one field. It was a spinner reading
"Loading field data…", which is a different picture from every other
wait on the page and announces a delay the reader cannot act on. */
return <FormFieldsSkeleton rows={1} />;
return <FormFieldsSkeleton rows={1} reason='arg-schema' />;
}

const renderAllowedValues = (currentType: IQorusType) => {
Expand Down
Loading