From ddd4ce233bfac7b67dfb29bcd7e41bb8a21595e8 Mon Sep 17 00:00:00 2001 From: Nick Date: Thu, 8 Oct 2026 11:33:12 +0200 Subject: [PATCH] fix(form): a field skeleton always says what it waits for FormFieldsSkeleton rendered `data-wait` only when its caller named a reason, and the two nested waits named none: AutoField resolving a named arg_schema, and the hash field loading its sub-schema. `data-wait` is also how a page asks whether a form has finished loading, so those placeholders were ones nothing could wait for; qorus-ide's Qlip build 279 captured the AI Guardrail creator's Rules row on one. `reason` now defaults to `loading`, so the skeleton is always marked, and both callers name `arg-schema`. 0.10.61. Closes #138 --- .../formFieldsSkeletonNamesItsWait.test.tsx | 61 +++++++++++++++++++ package.json | 2 +- .../form/engine/FormFieldsSkeleton.tsx | 7 ++- src/components/form/fields/Field.tsx | 2 +- .../form/fields/auto/AutoFormField.tsx | 2 +- 5 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 __tests__/formFieldsSkeletonNamesItsWait.test.tsx diff --git a/__tests__/formFieldsSkeletonNamesItsWait.test.tsx b/__tests__/formFieldsSkeletonNamesItsWait.test.tsx new file mode 100644 index 00000000..119f9f46 --- /dev/null +++ b/__tests__/formFieldsSkeletonNamesItsWait.test.tsx @@ -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(); + // 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) => ( + + {node} + +); + +describe('FormFieldsSkeleton', () => { + it('names the wait it stands for', () => { + const { container } = render(wrap()); + + expect(container.querySelector('[data-wait]')?.getAttribute('data-wait')).toBe('templates'); + }); + + it('is still marked when its caller names no wait', () => { + const { container } = render(wrap()); + + 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( + + ) + ); + + await waitFor(() => expect(container.querySelector('[data-wait="arg-schema"]')).not.toBeNull()); + }); +}); diff --git a/package.json b/package.json index e99de2e4..a39626f5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/form/engine/FormFieldsSkeleton.tsx b/src/components/form/engine/FormFieldsSkeleton.tsx index 0200a6d6..a1534753 100644 --- a/src/components/form/engine/FormFieldsSkeleton.tsx +++ b/src/components/form/engine/FormFieldsSkeleton.tsx @@ -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; } @@ -50,7 +55,7 @@ export const FormFieldsSkeleton = ({ rows = 6, fill, className, - reason, + reason = 'loading', }: IFormFieldsSkeletonProps) => ( ({ case 'hash': { if (argSchemaLoading) { // A hash renders a nested FORM, so it waits in the shape of one. - return ; + return ; } if (finalArgSchema) { return ( diff --git a/src/components/form/fields/auto/AutoFormField.tsx b/src/components/form/fields/auto/AutoFormField.tsx index 9238a1f8..aad4fbfb 100644 --- a/src/components/form/fields/auto/AutoFormField.tsx +++ b/src/components/form/fields/auto/AutoFormField.tsx @@ -511,7 +511,7 @@ function AutoField({ /* 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 ; + return ; } const renderAllowedValues = (currentType: IQorusType) => {