@qoretechnologies/reqraft 0.10.53 → 0.10.54-pr.115.g9bc96ed
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.md +25 -0
- package/.tasks/INDEX.md +1 -0
- package/.tasks/STORY_NETWORK_ISOLATION.md +98 -0
- package/README.md +1 -1
- package/design/FORM_ENGINE_EXPRESSIONS.md +110 -0
- package/design/FORM_ENGINE_FIELD_MIGRATION.md +56 -1
- package/design/RENDER_EXPRESSION_TRANSPORT.md +76 -10
- package/design/SMART_EDITOR_UX.md +21 -0
- package/dist/components/dpqlEditor/DpqlEditor.d.ts.map +1 -1
- package/dist/components/dpqlEditor/DpqlEditor.js +3 -3
- package/dist/components/dpqlEditor/DpqlEditor.js.map +1 -1
- package/dist/components/dpqlEditor/dpqlHelpers.d.ts +6 -0
- package/dist/components/dpqlEditor/dpqlHelpers.d.ts.map +1 -1
- package/dist/components/dpqlEditor/dpqlHelpers.js +21 -2
- package/dist/components/dpqlEditor/dpqlHelpers.js.map +1 -1
- package/dist/components/dpqlEditor/dpqlProbe.d.ts +37 -0
- package/dist/components/dpqlEditor/dpqlProbe.d.ts.map +1 -0
- package/dist/components/dpqlEditor/dpqlProbe.js +203 -0
- package/dist/components/dpqlEditor/dpqlProbe.js.map +1 -0
- package/dist/components/dpqlEditor/dpqlTags.d.ts +8 -0
- package/dist/components/dpqlEditor/dpqlTags.d.ts.map +1 -1
- package/dist/components/dpqlEditor/dpqlTags.js +5 -1
- package/dist/components/dpqlEditor/dpqlTags.js.map +1 -1
- package/dist/components/dpqlEditor/index.d.ts +4 -0
- package/dist/components/dpqlEditor/index.d.ts.map +1 -1
- package/dist/components/dpqlEditor/index.js +5 -1
- package/dist/components/dpqlEditor/index.js.map +1 -1
- package/dist/components/dpqlEditor/types.d.ts +42 -2
- package/dist/components/dpqlEditor/types.d.ts.map +1 -1
- package/dist/components/dpqlEditor/types.js +0 -4
- package/dist/components/dpqlEditor/types.js.map +1 -1
- package/dist/components/dpqlEditor/useDpqlProbe.d.ts +4 -0
- package/dist/components/dpqlEditor/useDpqlProbe.d.ts.map +1 -0
- package/dist/components/dpqlEditor/useDpqlProbe.js +29 -0
- package/dist/components/dpqlEditor/useDpqlProbe.js.map +1 -0
- package/dist/components/dpqlEditor/useDpqlSession.d.ts +5 -2
- package/dist/components/dpqlEditor/useDpqlSession.d.ts.map +1 -1
- package/dist/components/dpqlEditor/useDpqlSession.js +8 -2
- package/dist/components/dpqlEditor/useDpqlSession.js.map +1 -1
- package/dist/components/form/engine/CompactRow.d.ts.map +1 -1
- package/dist/components/form/engine/CompactRow.js +192 -45
- package/dist/components/form/engine/CompactRow.js.map +1 -1
- package/dist/components/form/engine/CompactToolbar.d.ts.map +1 -1
- package/dist/components/form/engine/CompactToolbar.js +5 -2
- package/dist/components/form/engine/CompactToolbar.js.map +1 -1
- package/dist/components/form/engine/FormEngine.d.ts +160 -12
- package/dist/components/form/engine/FormEngine.d.ts.map +1 -1
- package/dist/components/form/engine/FormEngine.js +376 -75
- package/dist/components/form/engine/FormEngine.js.map +1 -1
- package/dist/components/form/engine/FormFieldsSkeleton.d.ts +48 -0
- package/dist/components/form/engine/FormFieldsSkeleton.d.ts.map +1 -0
- package/dist/components/form/engine/FormFieldsSkeleton.js +38 -0
- package/dist/components/form/engine/FormFieldsSkeleton.js.map +1 -0
- package/dist/components/form/engine/OptionFieldMessages.d.ts +17 -2
- package/dist/components/form/engine/OptionFieldMessages.d.ts.map +1 -1
- package/dist/components/form/engine/OptionFieldMessages.js +61 -11
- package/dist/components/form/engine/OptionFieldMessages.js.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.d.ts.map +1 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js +7 -1
- package/dist/components/form/engine/_structuredData/SchemaDataView.js.map +1 -1
- package/dist/components/form/engine/compactToolbarContext.d.ts +1 -0
- package/dist/components/form/engine/compactToolbarContext.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.d.ts +42 -1
- package/dist/components/form/engine/readFirst.d.ts.map +1 -1
- package/dist/components/form/engine/readFirst.js +74 -7
- package/dist/components/form/engine/readFirst.js.map +1 -1
- package/dist/components/form/engine/rendererTypes.d.ts +13 -0
- package/dist/components/form/engine/rendererTypes.d.ts.map +1 -1
- package/dist/components/form/engine/rendererTypes.js.map +1 -1
- package/dist/components/form/engine/rowMenuContext.d.ts +78 -0
- package/dist/components/form/engine/rowMenuContext.d.ts.map +1 -0
- package/dist/components/form/engine/rowMenuContext.js +103 -0
- package/dist/components/form/engine/rowMenuContext.js.map +1 -0
- package/dist/components/form/engine/typeChoices.d.ts +37 -0
- package/dist/components/form/engine/typeChoices.d.ts.map +1 -0
- package/dist/components/form/engine/typeChoices.js +25 -0
- package/dist/components/form/engine/typeChoices.js.map +1 -0
- package/dist/components/form/expressions/DpqlRendering.d.ts +26 -0
- package/dist/components/form/expressions/DpqlRendering.d.ts.map +1 -0
- package/dist/components/form/expressions/DpqlRendering.js +53 -0
- package/dist/components/form/expressions/DpqlRendering.js.map +1 -0
- package/dist/components/form/expressions/ExpressionField.d.ts +7 -1
- package/dist/components/form/expressions/ExpressionField.d.ts.map +1 -1
- package/dist/components/form/expressions/ExpressionField.js +202 -45
- package/dist/components/form/expressions/ExpressionField.js.map +1 -1
- package/dist/components/form/expressions/argumentPresence.d.ts +97 -0
- package/dist/components/form/expressions/argumentPresence.d.ts.map +1 -0
- package/dist/components/form/expressions/argumentPresence.js +203 -0
- package/dist/components/form/expressions/argumentPresence.js.map +1 -0
- package/dist/components/form/expressions/builder/argumentLabel.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/argumentLabel.js +9 -4
- package/dist/components/form/expressions/builder/argumentLabel.js.map +1 -1
- package/dist/components/form/expressions/builder/index.d.ts +15 -2
- package/dist/components/form/expressions/builder/index.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/index.js +20 -58
- package/dist/components/form/expressions/builder/index.js.map +1 -1
- package/dist/components/form/expressions/builder/renderTemplate.d.ts +4 -1
- package/dist/components/form/expressions/builder/renderTemplate.d.ts.map +1 -1
- package/dist/components/form/expressions/builder/renderTemplate.js +10 -67
- package/dist/components/form/expressions/builder/renderTemplate.js.map +1 -1
- package/dist/components/form/expressions/dpqlMockLanguage.d.ts +76 -0
- package/dist/components/form/expressions/dpqlMockLanguage.d.ts.map +1 -0
- package/dist/components/form/expressions/dpqlMockLanguage.js +525 -0
- package/dist/components/form/expressions/dpqlMockLanguage.js.map +1 -0
- package/dist/components/form/expressions/dpqlMockLsp.d.ts +31 -2
- package/dist/components/form/expressions/dpqlMockLsp.d.ts.map +1 -1
- package/dist/components/form/expressions/dpqlMockLsp.js +86 -80
- package/dist/components/form/expressions/dpqlMockLsp.js.map +1 -1
- package/dist/components/form/expressions/mockExpressions.d.ts.map +1 -1
- package/dist/components/form/expressions/mockExpressions.js +8 -4
- package/dist/components/form/expressions/mockExpressions.js.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts +1 -1
- package/dist/components/form/expressions/renderExpressionToText.d.ts.map +1 -1
- package/dist/components/form/expressions/renderExpressionToText.js +95 -19
- package/dist/components/form/expressions/renderExpressionToText.js.map +1 -1
- package/dist/components/form/expressions/types.d.ts +7 -0
- package/dist/components/form/expressions/types.d.ts.map +1 -1
- package/dist/components/form/expressions/useRenderExpression.d.ts +7 -16
- package/dist/components/form/expressions/useRenderExpression.d.ts.map +1 -1
- package/dist/components/form/expressions/useRenderExpression.js +50 -126
- package/dist/components/form/expressions/useRenderExpression.js.map +1 -1
- package/dist/components/form/fields/Field.d.ts.map +1 -1
- package/dist/components/form/fields/Field.js +3 -1
- package/dist/components/form/fields/Field.js.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts +11 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.d.ts.map +1 -1
- package/dist/components/form/fields/allowed-values/AllowedValues.js +14 -2
- package/dist/components/form/fields/allowed-values/AllowedValues.js.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.d.ts.map +1 -1
- package/dist/components/form/fields/auto/AutoFormField.js +32 -8
- package/dist/components/form/fields/auto/AutoFormField.js.map +1 -1
- package/dist/components/form/fields/date/Date.d.ts +1 -1
- package/dist/components/form/fields/date/Date.d.ts.map +1 -1
- package/dist/components/form/fields/date/Date.js +16 -2
- package/dist/components/form/fields/date/Date.js.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts +21 -1
- package/dist/components/form/fields/rich-text/RichText.d.ts.map +1 -1
- package/dist/components/form/fields/rich-text/RichText.js +140 -8
- package/dist/components/form/fields/rich-text/RichText.js.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts +2 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.d.ts.map +1 -1
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js +3 -2
- package/dist/components/form/fields/template/ReadOnlyTemplateTag.js.map +1 -1
- package/dist/components/form/fields/template/TemplateField.d.ts +31 -1
- package/dist/components/form/fields/template/TemplateField.d.ts.map +1 -1
- package/dist/components/form/fields/template/TemplateField.js +528 -50
- package/dist/components/form/fields/template/TemplateField.js.map +1 -1
- package/dist/components/form/index.d.ts +1 -0
- package/dist/components/form/index.d.ts.map +1 -1
- package/dist/components/form/index.js +1 -0
- package/dist/components/form/index.js.map +1 -1
- package/dist/components/smartEditor/SmartEditor.d.ts.map +1 -1
- package/dist/components/smartEditor/SmartEditor.js +29 -8
- package/dist/components/smartEditor/SmartEditor.js.map +1 -1
- package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts +8 -0
- package/dist/components/smartEditor/__fixtures__/mockLspServer.d.ts.map +1 -1
- package/dist/components/smartEditor/__fixtures__/mockLspServer.js +53 -10
- package/dist/components/smartEditor/__fixtures__/mockLspServer.js.map +1 -1
- package/dist/components/smartEditor/useLspSession.d.ts +5 -1
- package/dist/components/smartEditor/useLspSession.d.ts.map +1 -1
- package/dist/components/smartEditor/useLspSession.js +16 -2
- package/dist/components/smartEditor/useLspSession.js.map +1 -1
- package/dist/helpers/dpqlDetection.d.ts +58 -0
- package/dist/helpers/dpqlDetection.d.ts.map +1 -0
- package/dist/helpers/dpqlDetection.js +164 -0
- package/dist/helpers/dpqlDetection.js.map +1 -0
- package/dist/helpers/optionUiTypes.d.ts +3 -3
- package/dist/helpers/options.d.ts +37 -0
- package/dist/helpers/options.d.ts.map +1 -1
- package/dist/helpers/options.js +78 -1
- package/dist/helpers/options.js.map +1 -1
- package/dist/helpers/templateItems.d.ts +47 -0
- package/dist/helpers/templateItems.d.ts.map +1 -0
- package/dist/helpers/templateItems.js +88 -0
- package/dist/helpers/templateItems.js.map +1 -0
- package/dist/helpers/templateText.d.ts +37 -0
- package/dist/helpers/templateText.d.ts.map +1 -0
- package/dist/helpers/templateText.js +64 -0
- package/dist/helpers/templateText.js.map +1 -0
- package/dist/helpers/templates.d.ts +19 -11
- package/dist/helpers/templates.d.ts.map +1 -1
- package/dist/helpers/templates.js +46 -29
- package/dist/helpers/templates.js.map +1 -1
- package/dist/helpers/validations.d.ts.map +1 -1
- package/dist/helpers/validations.js +56 -9
- package/dist/helpers/validations.js.map +1 -1
- package/dist/hooks/useArgSchema.d.ts +2 -1
- package/dist/hooks/useArgSchema.d.ts.map +1 -1
- package/dist/hooks/useFetch/useFetch.d.ts +3 -1
- package/dist/hooks/useFetch/useFetch.d.ts.map +1 -1
- package/dist/hooks/useFetch/useFetch.js +47 -9
- package/dist/hooks/useFetch/useFetch.js.map +1 -1
- package/dist/hooks/useTemplates.d.ts +20 -0
- package/dist/hooks/useTemplates.d.ts.map +1 -1
- package/dist/hooks/useTemplates.js +34 -2
- package/dist/hooks/useTemplates.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/stories/storyNetwork.d.ts +36 -0
- package/dist/stories/storyNetwork.d.ts.map +1 -0
- package/dist/stories/storyNetwork.js +82 -0
- package/dist/stories/storyNetwork.js.map +1 -0
- package/dist/utils/fetch.d.ts +151 -1
- package/dist/utils/fetch.d.ts.map +1 -1
- package/dist/utils/fetch.js +256 -44
- package/dist/utils/fetch.js.map +1 -1
- package/dist/utils/lspClient.d.ts +6 -0
- package/dist/utils/lspClient.d.ts.map +1 -1
- package/dist/utils/lspClient.js +62 -11
- package/dist/utils/lspClient.js.map +1 -1
- package/dist/utils/websocket.d.ts +6 -0
- package/dist/utils/websocket.d.ts.map +1 -1
- package/dist/utils/websocket.js +15 -7
- package/dist/utils/websocket.js.map +1 -1
- package/package.json +2 -2
- package/src/components/dpqlEditor/DpqlEditor.stories.tsx +3 -119
- package/src/components/dpqlEditor/DpqlEditor.tsx +5 -2
- package/src/components/dpqlEditor/dpqlHelpers.ts +20 -1
- package/src/components/dpqlEditor/dpqlProbe.ts +165 -0
- package/src/components/dpqlEditor/dpqlTags.ts +13 -0
- package/src/components/dpqlEditor/index.ts +4 -0
- package/src/components/dpqlEditor/types.ts +38 -2
- package/src/components/dpqlEditor/useDpqlProbe.ts +33 -0
- package/src/components/dpqlEditor/useDpqlSession.ts +20 -4
- package/src/components/form/engine/CompactRow.tsx +240 -51
- package/src/components/form/engine/CompactToolbar.tsx +10 -3
- package/src/components/form/engine/FormEngine.stories.tsx +423 -120
- package/src/components/form/engine/FormEngine.tsx +504 -87
- package/src/components/form/engine/FormFieldsSkeleton.tsx +76 -0
- package/src/components/form/engine/OptionFieldMessages.tsx +89 -11
- package/src/components/form/engine/_structuredData/SchemaDataView.tsx +11 -0
- package/src/components/form/engine/compactToolbarContext.ts +1 -1
- package/src/components/form/engine/readFirst.ts +86 -7
- package/src/components/form/engine/rendererTypes.ts +12 -0
- package/src/components/form/engine/rowMenuContext.ts +159 -0
- package/src/components/form/engine/typeChoices.ts +55 -0
- package/src/components/form/expressions/DpqlRendering.tsx +85 -0
- package/src/components/form/expressions/ExpressionField.stories.tsx +826 -48
- package/src/components/form/expressions/ExpressionField.tsx +265 -54
- package/src/components/form/expressions/argumentPresence.ts +200 -0
- package/src/components/form/expressions/builder/ExpressionBuilder.stories.tsx +20 -4
- package/src/components/form/expressions/builder/argumentLabel.tsx +10 -4
- package/src/components/form/expressions/builder/index.tsx +39 -77
- package/src/components/form/expressions/builder/renderTemplate.tsx +23 -56
- package/src/components/form/expressions/dpqlMockLanguage.ts +565 -0
- package/src/components/form/expressions/dpqlMockLsp.ts +109 -83
- package/src/components/form/expressions/mockExpressions.ts +8 -4
- package/src/components/form/expressions/renderExpressionToText.ts +112 -22
- package/src/components/form/expressions/types.ts +7 -0
- package/src/components/form/expressions/useRenderExpression.ts +43 -127
- package/src/components/form/fields/Field.tsx +3 -2
- package/src/components/form/fields/allowed-values/AllowedValues.tsx +13 -2
- package/src/components/form/fields/auto/AutoFormField.stories.tsx +27 -20
- package/src/components/form/fields/auto/AutoFormField.tsx +33 -9
- package/src/components/form/fields/date/Date.tsx +17 -1
- package/src/components/form/fields/rich-text/RichText.tsx +199 -22
- package/src/components/form/fields/template/ReadOnlyTemplateTag.tsx +4 -2
- package/src/components/form/fields/template/TemplateField.stories.tsx +31 -9
- package/src/components/form/fields/template/TemplateField.tsx +714 -122
- package/src/components/form/index.tsx +1 -0
- package/src/components/log/Log.stories.tsx +2 -1
- package/src/components/smartEditor/SmartEditor.tsx +41 -9
- package/src/components/smartEditor/__fixtures__/mockLspServer.ts +52 -3
- package/src/components/smartEditor/useLspSession.ts +21 -3
- package/src/helpers/dpqlDetection.ts +209 -0
- package/src/helpers/options.ts +70 -0
- package/src/helpers/templateItems.ts +81 -0
- package/src/helpers/templateText.ts +92 -0
- package/src/helpers/templates.ts +47 -37
- package/src/helpers/validations.ts +59 -9
- package/src/hooks/useFetch/useFetch.stories.tsx +6 -5
- package/src/hooks/useFetch/useFetch.tsx +53 -7
- package/src/hooks/useStorage/useStorage.stories.tsx +2 -1
- package/src/hooks/useTemplates.ts +41 -0
- package/src/hooks/useWebSocket/useWebsocket.stories.tsx +2 -1
- package/src/index.tsx +14 -1
- package/src/stores/currentUser/currentUser.stories.tsx +2 -1
- package/src/stories/storyNetwork.ts +92 -0
- package/src/utils/fetch.ts +311 -21
- package/src/utils/lspClient.ts +68 -11
- package/src/utils/websocket.ts +20 -9
|
@@ -2,11 +2,12 @@ import { ReqoreButton } from '@qoretechnologies/reqore';
|
|
|
2
2
|
import type { IReqoreEffect } from '@qoretechnologies/reqore/dist/components/Effect';
|
|
3
3
|
import { StoryObj } from '@storybook/react-vite';
|
|
4
4
|
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
|
|
5
|
-
import { useState } from 'react';
|
|
5
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
6
6
|
|
|
7
|
+
import { storyApiUrl } from '../../../stories/storyNetwork';
|
|
7
8
|
import { StoryMeta } from '../../../types';
|
|
8
9
|
import { FormEngine } from '../engine/FormEngine';
|
|
9
|
-
import {
|
|
10
|
+
import { dpqlMockParseCalls } from './dpqlMockLsp';
|
|
10
11
|
import { ExpressionField } from './ExpressionField';
|
|
11
12
|
import { mockExpressions } from './mockExpressions';
|
|
12
13
|
import { IExpression } from './types';
|
|
@@ -24,6 +25,9 @@ const IDE_AI_BUTTON_EFFECT: IReqoreEffect = {
|
|
|
24
25
|
},
|
|
25
26
|
};
|
|
26
27
|
|
|
28
|
+
/** A stable `onChange` for stories that own their value themselves. */
|
|
29
|
+
const noopChange = (): void => undefined;
|
|
30
|
+
|
|
27
31
|
const SAMPLE: IExpression = {
|
|
28
32
|
is_expression: true,
|
|
29
33
|
value: {
|
|
@@ -41,7 +45,7 @@ const meta = {
|
|
|
41
45
|
parameters: {
|
|
42
46
|
mockData: [
|
|
43
47
|
{
|
|
44
|
-
url: '
|
|
48
|
+
url: storyApiUrl('system?action=expressions&context=ui'),
|
|
45
49
|
method: 'GET',
|
|
46
50
|
status: 200,
|
|
47
51
|
response: mockExpressions,
|
|
@@ -101,16 +105,15 @@ export const Default: Story = {
|
|
|
101
105
|
|
|
102
106
|
// Visual mode renders the ported builder (with its own Explain); the shell
|
|
103
107
|
// adds only the Visual/Text toggle.
|
|
104
|
-
await waitFor(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
);
|
|
108
|
+
await waitFor(() => expect(canvasElement.querySelector('.expression')).toBeInTheDocument(), {
|
|
109
|
+
timeout: 6000,
|
|
110
|
+
});
|
|
108
111
|
await expect(await canvas.findByText('Logical Equals')).toBeInTheDocument();
|
|
109
112
|
},
|
|
110
113
|
};
|
|
111
114
|
|
|
112
115
|
/**
|
|
113
|
-
* Empty expression — an empty AST renders no "
|
|
116
|
+
* Empty expression — an empty AST renders no "Preview" box at all (nothing to
|
|
114
117
|
* parse), just the bare editor waiting for input.
|
|
115
118
|
*/
|
|
116
119
|
export const Empty: Story = {
|
|
@@ -122,20 +125,19 @@ export const Empty: Story = {
|
|
|
122
125
|
docs: {
|
|
123
126
|
description: {
|
|
124
127
|
story:
|
|
125
|
-
'Renders ExpressionField in Text mode with an empty expression AST — only the DPQL editor shows; the "
|
|
128
|
+
'Renders ExpressionField in Text mode with an empty expression AST — only the DPQL editor shows; the "Preview" box stays hidden until there is something to show.',
|
|
126
129
|
},
|
|
127
130
|
},
|
|
128
131
|
},
|
|
129
|
-
async beforeEach() {
|
|
130
|
-
const stop = startDpqlMockLsp();
|
|
131
|
-
return () => stop();
|
|
132
|
-
},
|
|
133
132
|
async play({ canvasElement }) {
|
|
134
133
|
const canvas = within(canvasElement);
|
|
135
134
|
await waitForLspIdle(canvasElement);
|
|
136
|
-
|
|
135
|
+
/* An empty query has nothing to render — no Preview box.
|
|
136
|
+
The testid is the assertion that matters: after the rename a
|
|
137
|
+
`queryByText('Parsed')` would be null whatever the box did, and an
|
|
138
|
+
assertion that cannot fail is worse than no assertion. */
|
|
137
139
|
await expect(canvas.queryByTestId('expression-preview')).toBeNull();
|
|
138
|
-
await expect(canvas.queryByText('
|
|
140
|
+
await expect(canvas.queryByText('Preview')).toBeNull();
|
|
139
141
|
},
|
|
140
142
|
};
|
|
141
143
|
|
|
@@ -255,10 +257,9 @@ export const ViaFormEngine: Story = {
|
|
|
255
257
|
async play({ canvasElement }) {
|
|
256
258
|
const canvas = within(canvasElement);
|
|
257
259
|
// The ported builder rendered inside the engine-driven form.
|
|
258
|
-
await waitFor(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
);
|
|
260
|
+
await waitFor(() => expect(canvasElement.querySelector('.expression')).toBeInTheDocument(), {
|
|
261
|
+
timeout: 6000,
|
|
262
|
+
});
|
|
262
263
|
await expect(await canvas.findByText('Logical Equals')).toBeInTheDocument();
|
|
263
264
|
// The ExpressionField shell wraps it: the Visual/Text toggle is present.
|
|
264
265
|
await expect(await canvas.findByText('Visual')).toBeInTheDocument();
|
|
@@ -272,7 +273,7 @@ export const ViaFormEngine: Story = {
|
|
|
272
273
|
/**
|
|
273
274
|
* FormEngine → Text mode, the serialize direction: switching the
|
|
274
275
|
* engine-driven expression field to Text seeds the DPQL editor from the
|
|
275
|
-
* stored AST (`dpql/serialize` over the mock LSP) and the "
|
|
276
|
+
* stored AST (`dpql/serialize` over the mock LSP) and the "Preview" box
|
|
276
277
|
* renders the same AST.
|
|
277
278
|
*/
|
|
278
279
|
export const ViaFormEngineTextMode: Story = {
|
|
@@ -280,23 +281,18 @@ export const ViaFormEngineTextMode: Story = {
|
|
|
280
281
|
docs: {
|
|
281
282
|
description: {
|
|
282
283
|
story:
|
|
283
|
-
'Renders the FormEngine expression field, then switches to Text mode — the DPQL editor seeds from the stored AST via the mock LSP\'s dpql/serialize call
|
|
284
|
+
'Renders the FormEngine expression field, then switches to Text mode — the DPQL editor seeds from the stored AST via the mock LSP\'s dpql/serialize call, highlighted like typed text. The server\'s rendering reads exactly as that text, so no "Preview" box repeats it.',
|
|
284
285
|
},
|
|
285
286
|
},
|
|
286
287
|
},
|
|
287
288
|
render: ViaFormEngine.render,
|
|
288
|
-
async beforeEach() {
|
|
289
|
-
const stop = startDpqlMockLsp();
|
|
290
|
-
return () => stop();
|
|
291
|
-
},
|
|
292
289
|
async play({ canvasElement }) {
|
|
293
290
|
const canvas = within(canvasElement);
|
|
294
291
|
|
|
295
292
|
// Visual (the builder) renders first.
|
|
296
|
-
await waitFor(
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
);
|
|
293
|
+
await waitFor(() => expect(canvasElement.querySelector('.expression')).toBeInTheDocument(), {
|
|
294
|
+
timeout: 6000,
|
|
295
|
+
});
|
|
300
296
|
|
|
301
297
|
// Switch to Text — the DPQL editor mounts and seeds from the AST
|
|
302
298
|
// (`dpql/serialize` over the mock LSP).
|
|
@@ -318,10 +314,27 @@ export const ViaFormEngineTextMode: Story = {
|
|
|
318
314
|
},
|
|
319
315
|
{ timeout: 10000 }
|
|
320
316
|
);
|
|
317
|
+
// …and highlighted: the server colours its own copy of the text, which it
|
|
318
|
+
// only holds if a value SET after mount reaches it as well as typing does.
|
|
321
319
|
await waitFor(
|
|
322
|
-
() =>
|
|
320
|
+
() => {
|
|
321
|
+
const literal = Array.from(editable.querySelectorAll<HTMLElement>('span[style]')).find(
|
|
322
|
+
(span) => span.textContent === '"John"' && !!span.style.color
|
|
323
|
+
);
|
|
324
|
+
expect(literal, 'the seeded text is coloured by the server').toBeTruthy();
|
|
325
|
+
},
|
|
323
326
|
{ timeout: 10000 }
|
|
324
327
|
);
|
|
328
|
+
/* The server renders `$local:name == "John"`, which reads exactly as the
|
|
329
|
+
text above it — the chip draws over the quotes — so there is no Preview. */
|
|
330
|
+
await waitFor(
|
|
331
|
+
() =>
|
|
332
|
+
expect(canvasElement.querySelector('.expression-field')?.getAttribute('data-preview')).toBe(
|
|
333
|
+
'repeats'
|
|
334
|
+
),
|
|
335
|
+
{ timeout: 10000 }
|
|
336
|
+
);
|
|
337
|
+
await expect(canvas.queryByTestId('expression-preview')).toBeNull();
|
|
325
338
|
await waitForLspIdle(canvasElement);
|
|
326
339
|
},
|
|
327
340
|
};
|
|
@@ -339,7 +352,7 @@ export const ViaFormEngineTextTyping: Story = {
|
|
|
339
352
|
docs: {
|
|
340
353
|
description: {
|
|
341
354
|
story:
|
|
342
|
-
|
|
355
|
+
"Renders the FormEngine expression field with an empty AST in Text mode. Typing DPQL text triggers the mock LSP's dpql/parse and the parsed expression lands in the form value with is_expression set.",
|
|
343
356
|
},
|
|
344
357
|
},
|
|
345
358
|
},
|
|
@@ -370,10 +383,6 @@ export const ViaFormEngineTextTyping: Story = {
|
|
|
370
383
|
/>
|
|
371
384
|
);
|
|
372
385
|
},
|
|
373
|
-
async beforeEach() {
|
|
374
|
-
const stop = startDpqlMockLsp();
|
|
375
|
-
return () => stop();
|
|
376
|
-
},
|
|
377
386
|
async play({ canvasElement, args }) {
|
|
378
387
|
const canvas = within(canvasElement);
|
|
379
388
|
|
|
@@ -403,17 +412,215 @@ export const ViaFormEngineTextTyping: Story = {
|
|
|
403
412
|
},
|
|
404
413
|
{ timeout: 10000 }
|
|
405
414
|
);
|
|
415
|
+
await waitFor(() => expect(canvas.getByTestId('expression-preview')).toBeInTheDocument(), {
|
|
416
|
+
timeout: 10000,
|
|
417
|
+
});
|
|
418
|
+
await waitForLspIdle(canvasElement);
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* A field's declared type travels with every parse, and the answer is shown
|
|
424
|
+
* only where there is something to decide.
|
|
425
|
+
*
|
|
426
|
+
* The mock returns type analysis ONLY when the request carried a
|
|
427
|
+
* `target_type`, exactly as the server does - so these stories fail if the
|
|
428
|
+
* field stops sending it, rather than passing on a fixture.
|
|
429
|
+
*/
|
|
430
|
+
/* `text` is per-story, and that is not cosmetic. `DpqlProbe` caches parse
|
|
431
|
+
results in a session-wide singleton keyed by the TEXT ALONE, so two stories
|
|
432
|
+
typing the same string share one answer: the second gets a cache HIT, sends
|
|
433
|
+
no request at all, and is handed the answer computed for the OTHER field's
|
|
434
|
+
type. That is order-dependent, so it passed locally and failed on CI, where
|
|
435
|
+
the two ran the other way round — and the instrumentation proved it by
|
|
436
|
+
reporting `parses=[]` for a field that was nonetheless showing a parsed
|
|
437
|
+
value. Distinct text per story keeps them independent. */
|
|
438
|
+
const typedExpressionStory = (
|
|
439
|
+
fieldType: string,
|
|
440
|
+
text: string,
|
|
441
|
+
description: string,
|
|
442
|
+
expectation: (canvasElement: HTMLElement) => Promise<void>
|
|
443
|
+
): Story => ({
|
|
444
|
+
parameters: {
|
|
445
|
+
docs: { description: { story: description } },
|
|
446
|
+
},
|
|
447
|
+
render: () => {
|
|
448
|
+
const [value, setValue] = useState<any>({
|
|
449
|
+
amount: { type: fieldType, value: { args: [] }, is_expression: true },
|
|
450
|
+
});
|
|
451
|
+
return (
|
|
452
|
+
<FormEngine
|
|
453
|
+
name='typedExprForm'
|
|
454
|
+
options={
|
|
455
|
+
{
|
|
456
|
+
amount: {
|
|
457
|
+
type: fieldType,
|
|
458
|
+
ui_type: fieldType,
|
|
459
|
+
display_name: 'Amount',
|
|
460
|
+
preselected: true,
|
|
461
|
+
supports_expressions: true,
|
|
462
|
+
expressions: mockExpressions,
|
|
463
|
+
},
|
|
464
|
+
} as any
|
|
465
|
+
}
|
|
466
|
+
value={value}
|
|
467
|
+
onChange={(_n, v) => setValue(v)}
|
|
468
|
+
/>
|
|
469
|
+
);
|
|
470
|
+
},
|
|
471
|
+
async play({ canvasElement }) {
|
|
472
|
+
const canvas = within(canvasElement);
|
|
473
|
+
await userEvent.click(await canvas.findByText('Text'));
|
|
474
|
+
const editable = (await waitFor(
|
|
475
|
+
() => {
|
|
476
|
+
const el = canvasElement.querySelector('[contenteditable="true"]');
|
|
477
|
+
if (!el) throw new Error('editor not ready');
|
|
478
|
+
return el as HTMLElement;
|
|
479
|
+
},
|
|
480
|
+
{ timeout: 10000 }
|
|
481
|
+
)) as HTMLElement;
|
|
482
|
+
/* The editor mounting is NOT the same as the language server being ready,
|
|
483
|
+
and typing into it before the socket is up loses the parse: no request is
|
|
484
|
+
sent, so no `target_type` answer ever comes back and the fit message is
|
|
485
|
+
never rendered. That is a race, not a slow response — raising the
|
|
486
|
+
expectation's timeout would not have helped.
|
|
487
|
+
|
|
488
|
+
It cost a red CI run that was green locally, where the mock connects
|
|
489
|
+
immediately. Worse, it is silent in the sibling story: one that asserts
|
|
490
|
+
the ABSENCE of the message passes for the wrong reason under exactly the
|
|
491
|
+
same race. */
|
|
492
|
+
await waitForLspIdle(canvasElement);
|
|
493
|
+
await userEvent.click(editable);
|
|
494
|
+
await userEvent.type(editable, text);
|
|
495
|
+
await expectation(canvasElement);
|
|
496
|
+
await waitForLspIdle(canvasElement);
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Text used as a number: the conversion is attempted, not guaranteed, so the
|
|
502
|
+
* field says so and offers the conversion. The value itself is checked again
|
|
503
|
+
* server-side when the expression actually runs.
|
|
504
|
+
*/
|
|
505
|
+
export const TextModeTypeMayNotFit: Story = typedExpressionStory(
|
|
506
|
+
'int',
|
|
507
|
+
'"a" + 1',
|
|
508
|
+
'Types `"a" + 1` into the Text view of an int field. The expression returns text, so the field warns "This may not fit" and offers the `toInt(…)` conversion.',
|
|
509
|
+
async (canvasElement) => {
|
|
510
|
+
/* Matched against the rendered TEXT, not with `getByText`. `ReqoreMessage`
|
|
511
|
+
renders its title through nested nodes, so an exact single-element match
|
|
512
|
+
reports "unable to find" for a message that is demonstrably on screen —
|
|
513
|
+
the very failure testing-library's own error describes ("the text is
|
|
514
|
+
broken up by multiple elements"). A unit test against this component
|
|
515
|
+
reproduced that directly: the container held the title, the message and
|
|
516
|
+
the fix chip, and `getByText` still could not find it.
|
|
517
|
+
|
|
518
|
+
The assertion stays meaningful: the title has to be present AND the
|
|
519
|
+
suggested conversion has to be the one for this field's type. */
|
|
520
|
+
/* The assertion carries the diagnosis with it. A DOM dump is truncated by
|
|
521
|
+
testing-library and a `console.log` does not reliably reach a CI log, but
|
|
522
|
+
a custom matcher message always does — and the one thing worth knowing
|
|
523
|
+
here is whether the front end SENT `target_type` at all, because the mock
|
|
524
|
+
answers with analysis only when it did. Without this the failure looks
|
|
525
|
+
identical whether the request lacked a target or the answer went
|
|
526
|
+
unrendered. */
|
|
527
|
+
const why = () =>
|
|
528
|
+
`parses=${JSON.stringify(dpqlMockParseCalls)} text=${JSON.stringify(
|
|
529
|
+
(canvasElement.textContent || '').slice(0, 400)
|
|
530
|
+
)}`;
|
|
406
531
|
await waitFor(
|
|
407
|
-
() =>
|
|
532
|
+
() => {
|
|
533
|
+
expect(canvasElement.textContent, why()).toContain('This may not fit');
|
|
534
|
+
expect(
|
|
535
|
+
canvasElement.querySelector('[data-testid="expression-type-fix"]')?.textContent,
|
|
536
|
+
why()
|
|
537
|
+
).toContain('toInt(');
|
|
538
|
+
},
|
|
539
|
+
{ timeout: 10000 }
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Text used as text needs no conversion at all, so nothing is said. A warning
|
|
546
|
+
* here would be noise, and noise is what teaches people to ignore the
|
|
547
|
+
* warnings that matter.
|
|
548
|
+
*/
|
|
549
|
+
export const TextModeTypeFits: Story = typedExpressionStory(
|
|
550
|
+
'string',
|
|
551
|
+
'"a" + "b"',
|
|
552
|
+
'Types `"a" + "b"` into the Text view of a string field. The expression already returns text, so no fit message is shown.',
|
|
553
|
+
async (canvasElement) => {
|
|
554
|
+
/* Settled when the server has analysed the WHOLE text against this field's
|
|
555
|
+
type — not when a Preview appears, which is shown only when the rendering
|
|
556
|
+
differs from what was typed and so says nothing about the analysis. */
|
|
557
|
+
await waitFor(
|
|
558
|
+
() =>
|
|
559
|
+
expect(
|
|
560
|
+
dpqlMockParseCalls.some((call) => call.text === '"a" + "b"' && call.target === 'string'),
|
|
561
|
+
`parses=${JSON.stringify(dpqlMockParseCalls)}`
|
|
562
|
+
).toBe(true),
|
|
408
563
|
{ timeout: 10000 }
|
|
409
564
|
);
|
|
410
565
|
await waitForLspIdle(canvasElement);
|
|
411
|
-
|
|
412
|
-
|
|
566
|
+
/* Absence, checked against the rendered text for the same reason its sibling
|
|
567
|
+
checks presence that way: `queryByText` returns null for a message that IS
|
|
568
|
+
on screen but split across nodes, so it would report "no warning" whether
|
|
569
|
+
or not one was drawn — passing for the wrong reason is worse here than
|
|
570
|
+
failing, because absence is the whole claim. */
|
|
571
|
+
expect(canvasElement.textContent).not.toContain('This may not fit');
|
|
572
|
+
expect(canvasElement.textContent).not.toContain('This does not fit');
|
|
573
|
+
}
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* An `auto` field accepts anything, so there is nothing to check the
|
|
578
|
+
* expression's return type against and no target is sent at all.
|
|
579
|
+
*
|
|
580
|
+
* This is not a hypothetical. An expression VALUE is
|
|
581
|
+
* `{ is_expression: true, value: {...} }` — a hash whatever the expression
|
|
582
|
+
* computes — and the auto field used to infer its type from that envelope, so
|
|
583
|
+
* writing an expression made the field decide it held a `hash`. That inferred
|
|
584
|
+
* type was then handed back as the return type to check against, and `1 + 2`
|
|
585
|
+
* was rejected on a test assertion's Value with "The expression returns int,
|
|
586
|
+
* and this field holds hash": the field disagreeing with itself about a value
|
|
587
|
+
* the author had just written.
|
|
588
|
+
*/
|
|
589
|
+
export const TextModeAutoAsksNothing: Story = typedExpressionStory(
|
|
590
|
+
'auto',
|
|
591
|
+
'1 + 2',
|
|
592
|
+
'Types `1 + 2` into the Text view of an `auto` field. The field accepts any result, so no return type is sent to the server and no fit message is shown.',
|
|
593
|
+
async (canvasElement) => {
|
|
594
|
+
await waitFor(
|
|
595
|
+
() => {
|
|
596
|
+
expect(canvasElement.querySelector('[contenteditable="true"]')).toBeTruthy();
|
|
597
|
+
},
|
|
598
|
+
{ timeout: 10000 }
|
|
599
|
+
);
|
|
600
|
+
/* The contract, not just the symptom: the mock answers with analysis only
|
|
601
|
+
when a target was sent, so asserting the MESSAGE is absent would also
|
|
602
|
+
pass if the request simply never happened. Assert what was asked. */
|
|
603
|
+
await waitFor(
|
|
604
|
+
() => {
|
|
605
|
+
expect(
|
|
606
|
+
dpqlMockParseCalls.length,
|
|
607
|
+
`parses=${JSON.stringify(dpqlMockParseCalls)}`
|
|
608
|
+
).toBeGreaterThan(0);
|
|
609
|
+
},
|
|
610
|
+
{ timeout: 10000 }
|
|
611
|
+
);
|
|
612
|
+
expect(
|
|
613
|
+
dpqlMockParseCalls.every((call) => !call.target),
|
|
614
|
+
`an auto field must send no target_type: ${JSON.stringify(dpqlMockParseCalls)}`
|
|
615
|
+
).toBe(true);
|
|
616
|
+
expect(canvasElement.textContent).not.toContain('This does not fit');
|
|
617
|
+
expect(canvasElement.textContent).not.toContain('This may not fit');
|
|
618
|
+
}
|
|
619
|
+
);
|
|
413
620
|
|
|
414
621
|
/**
|
|
415
622
|
* Text (DPQL) mode, backed by a mock-socket LSP. Typing DPQL parses to the
|
|
416
|
-
* AST (`dpql/parse`); the "
|
|
623
|
+
* AST (`dpql/parse`); the "Preview" box reflects it. (Slate typing is
|
|
417
624
|
* driven live; the play test asserts the editor mounted + connected.)
|
|
418
625
|
*/
|
|
419
626
|
export const TextMode: Story = {
|
|
@@ -425,14 +632,10 @@ export const TextMode: Story = {
|
|
|
425
632
|
docs: {
|
|
426
633
|
description: {
|
|
427
634
|
story:
|
|
428
|
-
'Renders ExpressionField in Text (DPQL) mode over a mock-socket LSP. Typing DPQL text triggers dpql/parse and the "
|
|
635
|
+
'Renders ExpressionField in Text (DPQL) mode over a mock-socket LSP. Typing DPQL text triggers dpql/parse and the "Preview" box reflects the resulting AST.',
|
|
429
636
|
},
|
|
430
637
|
},
|
|
431
638
|
},
|
|
432
|
-
async beforeEach() {
|
|
433
|
-
const stop = startDpqlMockLsp();
|
|
434
|
-
return () => stop();
|
|
435
|
-
},
|
|
436
639
|
async play({ canvasElement }) {
|
|
437
640
|
const canvas = within(canvasElement);
|
|
438
641
|
const editable = (await waitFor(
|
|
@@ -444,7 +647,7 @@ export const TextMode: Story = {
|
|
|
444
647
|
{ timeout: 6000 }
|
|
445
648
|
)) as HTMLElement;
|
|
446
649
|
|
|
447
|
-
// Type DPQL → debounced `dpql/parse` → AST → the "
|
|
650
|
+
// Type DPQL → debounced `dpql/parse` → AST → the "Preview" box
|
|
448
651
|
// reflects it (the mock echoes the typed text into an `==` expression).
|
|
449
652
|
await userEvent.click(editable);
|
|
450
653
|
await userEvent.type(editable, 'name');
|
|
@@ -524,6 +727,581 @@ export const ToggleInFormEngine: Story = {
|
|
|
524
727
|
},
|
|
525
728
|
};
|
|
526
729
|
|
|
730
|
+
/**
|
|
731
|
+
* Wait for the DOM to stop changing.
|
|
732
|
+
*
|
|
733
|
+
* Both stories below assert a NEGATIVE — that the view the author was put in
|
|
734
|
+
* is not swapped out from under them — and a swap lands a render or two after
|
|
735
|
+
* the interaction that causes it. `waitFor` is therefore no help at all: it
|
|
736
|
+
* passes on the transient state BEFORE the swap and proves nothing. Settling
|
|
737
|
+
* on the DOM going quiet is what makes the assertion mean something.
|
|
738
|
+
*/
|
|
739
|
+
const settle = (root: HTMLElement, quietMs = 400): Promise<void> =>
|
|
740
|
+
new Promise<void>((resolve) => {
|
|
741
|
+
let quiet: ReturnType<typeof setTimeout>;
|
|
742
|
+
const stop = (): void => {
|
|
743
|
+
observer.disconnect();
|
|
744
|
+
resolve();
|
|
745
|
+
};
|
|
746
|
+
const observer = new MutationObserver(() => {
|
|
747
|
+
clearTimeout(quiet);
|
|
748
|
+
quiet = setTimeout(stop, quietMs);
|
|
749
|
+
});
|
|
750
|
+
observer.observe(root, {
|
|
751
|
+
attributes: true,
|
|
752
|
+
characterData: true,
|
|
753
|
+
childList: true,
|
|
754
|
+
subtree: true,
|
|
755
|
+
});
|
|
756
|
+
quiet = setTimeout(stop, quietMs);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
/** Open a compact row — a read-first row shows its label, not its editor. */
|
|
760
|
+
const openCompactRow = async (canvasElement: HTMLElement): Promise<void> => {
|
|
761
|
+
const label = (await waitFor(
|
|
762
|
+
() => {
|
|
763
|
+
const el = within(canvasElement).queryByText('Value');
|
|
764
|
+
if (!el) throw new Error('row not rendered');
|
|
765
|
+
return el as HTMLElement;
|
|
766
|
+
},
|
|
767
|
+
{ timeout: 10000 }
|
|
768
|
+
)) as HTMLElement;
|
|
769
|
+
if (!canvasElement.querySelector('input[type="text"], textarea, [contenteditable="true"]')) {
|
|
770
|
+
await userEvent.click(label);
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* Type an expression into a plain field, accept the offer, and assert the
|
|
776
|
+
* shell opens on TEXT and stays there.
|
|
777
|
+
*
|
|
778
|
+
* Shared so that each story below differs only in the FORM around the field —
|
|
779
|
+
* which is the variable under test, the plain engine being healthy and the
|
|
780
|
+
* arrangements a real consumer builds being the ones that break it.
|
|
781
|
+
*/
|
|
782
|
+
const acceptTypedExpression = async (canvasElement: HTMLElement): Promise<void> => {
|
|
783
|
+
const input = (await waitFor(
|
|
784
|
+
() => {
|
|
785
|
+
const el = canvasElement.querySelector(
|
|
786
|
+
'input[type="text"], textarea, [contenteditable="true"]'
|
|
787
|
+
);
|
|
788
|
+
if (!el) throw new Error('field not ready');
|
|
789
|
+
return el as HTMLElement;
|
|
790
|
+
},
|
|
791
|
+
{ timeout: 10000 }
|
|
792
|
+
)) as HTMLElement;
|
|
793
|
+
|
|
794
|
+
await userEvent.click(input);
|
|
795
|
+
await userEvent.type(input, '1 + 2');
|
|
796
|
+
|
|
797
|
+
// The offer appears only after the server (here the mock) has said the text
|
|
798
|
+
// really is an expression — a successful parse alone means nothing.
|
|
799
|
+
const offer = (await waitFor(
|
|
800
|
+
() => {
|
|
801
|
+
const el = canvasElement.querySelector('.dpql-detected-offer');
|
|
802
|
+
if (!el) throw new Error('offer not made');
|
|
803
|
+
return el as HTMLElement;
|
|
804
|
+
},
|
|
805
|
+
{ timeout: 10000 }
|
|
806
|
+
)) as HTMLElement;
|
|
807
|
+
|
|
808
|
+
await userEvent.click(offer);
|
|
809
|
+
|
|
810
|
+
await waitFor(() => expect(canvasElement.querySelector('.expression-field')).toBeInTheDocument(), {
|
|
811
|
+
timeout: 10000,
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
await settle(canvasElement);
|
|
815
|
+
|
|
816
|
+
// Text, not Visual: the DPQL editor is mounted and the builder is not.
|
|
817
|
+
const editable = canvasElement.querySelector(
|
|
818
|
+
'.expression-field [contenteditable="true"]'
|
|
819
|
+
) as HTMLElement | null;
|
|
820
|
+
expect(editable).toBeInTheDocument();
|
|
821
|
+
expect(canvasElement.querySelector('.expression-field .expression')).not.toBeInTheDocument();
|
|
822
|
+
|
|
823
|
+
/* And it holds the author's own sentence.
|
|
824
|
+
*
|
|
825
|
+
* Landing in Text mode with an EMPTY editor is barely better than landing in
|
|
826
|
+
* the builder: what the author typed is not on screen either way, and a
|
|
827
|
+
* Preview of it sitting beside an empty box reads as the text having been
|
|
828
|
+
* thrown away. The AST reaches this shell one render after the mode does, so
|
|
829
|
+
* this is exactly the assertion that fails when seeding only ever gets one
|
|
830
|
+
* turn. */
|
|
831
|
+
await waitFor(() => expect(editable?.textContent).toContain('1 + 2'), { timeout: 10000 });
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Accepting "Use as expression" lands in TEXT mode, and STAYS there.
|
|
836
|
+
*
|
|
837
|
+
* An author who typed `1 + 2` into a plain field and accepted the offer is
|
|
838
|
+
* already writing in that language: `TemplateField` hands the shell
|
|
839
|
+
* `defaultMode='text'` for exactly that reason. Landing them in the visual
|
|
840
|
+
* builder instead makes them find their own sentence again — and the sentence
|
|
841
|
+
* they typed is not shown anywhere, because the builder renders operands, not
|
|
842
|
+
* text.
|
|
843
|
+
*
|
|
844
|
+
* The whole path is covered deliberately: the offer is the ONLY entry that
|
|
845
|
+
* asks for Text mode, so a story that enters expression mode through the More
|
|
846
|
+
* menu (`ToggleInFormEngine`) cannot guard it — that route never sets
|
|
847
|
+
* `expressionFromText` and correctly lands on Visual.
|
|
848
|
+
*/
|
|
849
|
+
export const OfferAcceptedOpensTextMode: Story = {
|
|
850
|
+
parameters: {
|
|
851
|
+
docs: {
|
|
852
|
+
description: {
|
|
853
|
+
story:
|
|
854
|
+
'Types DPQL into a plain string field, accepts the "Use as expression" offer, and asserts the expression shell opens on Text (the DPQL editor seeded with the typed text) rather than on the visual builder — and stays there once the DOM settles.',
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
},
|
|
858
|
+
render: () => {
|
|
859
|
+
const [value, setValue] = useState<any>({});
|
|
860
|
+
return (
|
|
861
|
+
<FormEngine
|
|
862
|
+
name='dpqlOfferForm'
|
|
863
|
+
options={
|
|
864
|
+
{
|
|
865
|
+
expected: {
|
|
866
|
+
type: 'string',
|
|
867
|
+
ui_type: 'string',
|
|
868
|
+
display_name: 'Value',
|
|
869
|
+
preselected: true,
|
|
870
|
+
supports_expressions: true,
|
|
871
|
+
expressions: mockExpressions,
|
|
872
|
+
},
|
|
873
|
+
} as any
|
|
874
|
+
}
|
|
875
|
+
value={value}
|
|
876
|
+
onChange={(_n, v) => setValue(v)}
|
|
877
|
+
/>
|
|
878
|
+
);
|
|
879
|
+
},
|
|
880
|
+
async play({ canvasElement }) {
|
|
881
|
+
await acceptTypedExpression(canvasElement);
|
|
882
|
+
},
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* The accepted offer survives a host that rebuilds the field's schema.
|
|
887
|
+
*
|
|
888
|
+
* A consumer form does not hand the engine one frozen schema. qorus-ide's test
|
|
889
|
+
* step drawer derives the schema it passes FROM the current value — a path that
|
|
890
|
+
* holds a reference is given the reference editor, a compared value is typed by
|
|
891
|
+
* the path it is compared against — so the engine is handed a brand-new options
|
|
892
|
+
* object on every change, including the one the accepted offer itself causes.
|
|
893
|
+
*
|
|
894
|
+
* The offer's whole promise is that the author keeps writing where they were
|
|
895
|
+
* writing. If a re-derived schema takes that away, the promise is only kept on
|
|
896
|
+
* forms simple enough not to have one, which is not the form this feature was
|
|
897
|
+
* built for.
|
|
898
|
+
*/
|
|
899
|
+
export const OfferSurvivesARederivedSchema: Story = {
|
|
900
|
+
parameters: {
|
|
901
|
+
docs: {
|
|
902
|
+
description: {
|
|
903
|
+
story:
|
|
904
|
+
"Repeats the accepted-offer flow against a host that rebuilds the options schema from the value on every change (as qorus-ide's step drawer does) — the Text view the offer opened must survive the churn.",
|
|
905
|
+
},
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
render: () => {
|
|
909
|
+
const [value, setValue] = useState<any>({});
|
|
910
|
+
// Rebuilt from the value, so its identity changes on every edit — the
|
|
911
|
+
// host behaviour being modelled.
|
|
912
|
+
const options = useMemo(
|
|
913
|
+
() =>
|
|
914
|
+
({
|
|
915
|
+
expected: {
|
|
916
|
+
type: 'string',
|
|
917
|
+
ui_type: 'string',
|
|
918
|
+
display_name: 'Value',
|
|
919
|
+
preselected: true,
|
|
920
|
+
supports_expressions: true,
|
|
921
|
+
expressions: mockExpressions,
|
|
922
|
+
},
|
|
923
|
+
}) as any,
|
|
924
|
+
[value]
|
|
925
|
+
);
|
|
926
|
+
return (
|
|
927
|
+
<FormEngine
|
|
928
|
+
name='dpqlOfferChurnForm'
|
|
929
|
+
options={options}
|
|
930
|
+
value={value}
|
|
931
|
+
onChange={(_n, v) => setValue(v)}
|
|
932
|
+
/>
|
|
933
|
+
);
|
|
934
|
+
},
|
|
935
|
+
async play({ canvasElement }) {
|
|
936
|
+
await acceptTypedExpression(canvasElement);
|
|
937
|
+
},
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* The accepted offer survives the COMPACT read-first row.
|
|
942
|
+
*
|
|
943
|
+
* The forms this feature was built for are compact ones — qorus-ide's test step
|
|
944
|
+
* drawer renders every option as a read-first row that shows a summary of what
|
|
945
|
+
* the field holds and opens onto the editor. Accepting the offer is the moment
|
|
946
|
+
* a field that held nothing starts holding something, so it is exactly the
|
|
947
|
+
* moment that treatment changes, and the author is inside the control while it
|
|
948
|
+
* does.
|
|
949
|
+
*/
|
|
950
|
+
export const OfferSurvivesACompactRow: Story = {
|
|
951
|
+
parameters: {
|
|
952
|
+
docs: {
|
|
953
|
+
description: {
|
|
954
|
+
story:
|
|
955
|
+
'Repeats the accepted-offer flow in a compact (read-first) form whose schema is rebuilt from the value — the arrangement qorus-ide uses — and asserts the Text view survives it.',
|
|
956
|
+
},
|
|
957
|
+
},
|
|
958
|
+
},
|
|
959
|
+
render: () => {
|
|
960
|
+
const [value, setValue] = useState<any>({});
|
|
961
|
+
const options = useMemo(
|
|
962
|
+
() =>
|
|
963
|
+
({
|
|
964
|
+
expected: {
|
|
965
|
+
type: 'string',
|
|
966
|
+
ui_type: 'string',
|
|
967
|
+
display_name: 'Value',
|
|
968
|
+
preselected: true,
|
|
969
|
+
supports_expressions: true,
|
|
970
|
+
expressions: mockExpressions,
|
|
971
|
+
},
|
|
972
|
+
}) as any,
|
|
973
|
+
[value]
|
|
974
|
+
);
|
|
975
|
+
return (
|
|
976
|
+
<FormEngine
|
|
977
|
+
name='dpqlOfferCompactForm'
|
|
978
|
+
compact
|
|
979
|
+
flat
|
|
980
|
+
transparent
|
|
981
|
+
padded={false}
|
|
982
|
+
showTypeToggle={false}
|
|
983
|
+
size='small'
|
|
984
|
+
options={options}
|
|
985
|
+
value={value}
|
|
986
|
+
onChange={(_n, v) => setValue(v)}
|
|
987
|
+
/>
|
|
988
|
+
);
|
|
989
|
+
},
|
|
990
|
+
async play({ canvasElement }) {
|
|
991
|
+
await openCompactRow(canvasElement);
|
|
992
|
+
await acceptTypedExpression(canvasElement);
|
|
993
|
+
},
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
/**
|
|
997
|
+
* The Text view survives the host answering LATE about the field.
|
|
998
|
+
*
|
|
999
|
+
* A consumer does not settle a row's schema the moment it is drawn. qorus-ide
|
|
1000
|
+
* re-derives these rows from catalogues it is still fetching — the type a
|
|
1001
|
+
* compared value gets is read off the reference it is compared against — so the
|
|
1002
|
+
* type a row was OPENED with is not always the type it ends up with, and the
|
|
1003
|
+
* change can land a second or two after the author has already acted.
|
|
1004
|
+
*
|
|
1005
|
+
* That is the reported symptom this covers, and it is the one that made the bug
|
|
1006
|
+
* look intermittent rather than reproducible: clicking Text appeared to work,
|
|
1007
|
+
* and then a moment later the control was back on Visual with nothing on screen
|
|
1008
|
+
* to say why.
|
|
1009
|
+
*/
|
|
1010
|
+
export const TextViewSurvivesALateHostChange: Story = {
|
|
1011
|
+
parameters: {
|
|
1012
|
+
docs: {
|
|
1013
|
+
description: {
|
|
1014
|
+
story:
|
|
1015
|
+
"Accepts the offer in a compact row, then has the host re-type the field a moment later (as qorus-ide does when its reference catalogue answers) — the Text view must not be swapped out from under the author.",
|
|
1016
|
+
},
|
|
1017
|
+
},
|
|
1018
|
+
},
|
|
1019
|
+
render: () => {
|
|
1020
|
+
const [value, setValue] = useState<any>({});
|
|
1021
|
+
const [retyped, setRetyped] = useState(false);
|
|
1022
|
+
// Fires once the field is holding an expression: the host's answer lands
|
|
1023
|
+
// after the author has already accepted the offer.
|
|
1024
|
+
const isExpression = !!value?.expected?.is_expression;
|
|
1025
|
+
useEffect(() => {
|
|
1026
|
+
if (!isExpression) return undefined;
|
|
1027
|
+
const timer = setTimeout(() => setRetyped(true), 500);
|
|
1028
|
+
return () => clearTimeout(timer);
|
|
1029
|
+
}, [isExpression]);
|
|
1030
|
+
const options = useMemo(
|
|
1031
|
+
() =>
|
|
1032
|
+
({
|
|
1033
|
+
expected: {
|
|
1034
|
+
// `auto` is one of the types a compact row opens as a CARD rather
|
|
1035
|
+
// than in place, so this is a re-type that moves the editor — the
|
|
1036
|
+
// whole point of the exercise.
|
|
1037
|
+
type: retyped ? 'auto' : 'string',
|
|
1038
|
+
ui_type: retyped ? 'auto' : 'string',
|
|
1039
|
+
display_name: 'Value',
|
|
1040
|
+
preselected: true,
|
|
1041
|
+
supports_expressions: true,
|
|
1042
|
+
expressions: mockExpressions,
|
|
1043
|
+
},
|
|
1044
|
+
}) as any,
|
|
1045
|
+
[retyped]
|
|
1046
|
+
);
|
|
1047
|
+
return (
|
|
1048
|
+
/* The host's answer is not otherwise visible, and the assertion has to
|
|
1049
|
+
wait for it rather than for a duration. An attribute, not text: a
|
|
1050
|
+
printed "true" ended up in the story's snapshot. */
|
|
1051
|
+
<div data-host-retyped={String(retyped)}>
|
|
1052
|
+
<FormEngine
|
|
1053
|
+
name='dpqlOfferLateForm'
|
|
1054
|
+
compact
|
|
1055
|
+
flat
|
|
1056
|
+
transparent
|
|
1057
|
+
padded={false}
|
|
1058
|
+
showTypeToggle={false}
|
|
1059
|
+
size='small'
|
|
1060
|
+
options={options}
|
|
1061
|
+
value={value}
|
|
1062
|
+
onChange={(_n, v) => setValue(v)}
|
|
1063
|
+
/>
|
|
1064
|
+
</div>
|
|
1065
|
+
);
|
|
1066
|
+
},
|
|
1067
|
+
async play({ canvasElement }) {
|
|
1068
|
+
await openCompactRow(canvasElement);
|
|
1069
|
+
await acceptTypedExpression(canvasElement);
|
|
1070
|
+
|
|
1071
|
+
// Wait for the host's late answer to actually land — a timeout here would
|
|
1072
|
+
// make the assertion a race.
|
|
1073
|
+
await waitFor(
|
|
1074
|
+
() => expect(canvasElement.querySelector('[data-host-retyped="true"]')).toBeTruthy(),
|
|
1075
|
+
{ timeout: 10000 }
|
|
1076
|
+
);
|
|
1077
|
+
await settle(canvasElement);
|
|
1078
|
+
|
|
1079
|
+
expect(
|
|
1080
|
+
canvasElement.querySelector('.expression-field [contenteditable="true"]')
|
|
1081
|
+
).toBeInTheDocument();
|
|
1082
|
+
expect(canvasElement.querySelector('.expression-field .expression')).not.toBeInTheDocument();
|
|
1083
|
+
},
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* The field's own menu stays in the toolbar row.
|
|
1088
|
+
*
|
|
1089
|
+
* `TemplateField` renders its controls — the More menu that offers "Use
|
|
1090
|
+
* Template", and the undo for an accepted offer — as SIBLINGS of the expression
|
|
1091
|
+
* shell. Beside an ordinary one-line editor, centring that menu in the flex
|
|
1092
|
+
* line is what lines it up with the row's other buttons.
|
|
1093
|
+
*
|
|
1094
|
+
* The shell is not one line. It is a toolbar with an editor, a type message and
|
|
1095
|
+
* a preview stacked under it, so centring put the menu somewhere down the side
|
|
1096
|
+
* of that block — level with nothing, overlapping the editor, and a long way
|
|
1097
|
+
* from the Undo it belongs beside.
|
|
1098
|
+
*
|
|
1099
|
+
* Only reachable with templates ON OFFER, which is why the stories above miss
|
|
1100
|
+
* it: with nothing to offer the menu is not rendered at all.
|
|
1101
|
+
*
|
|
1102
|
+
* Inside a form row the field no longer draws that menu itself: it publishes
|
|
1103
|
+
* its items into the row's own ⋮ (`rowMenuContext` — one menu per control), so
|
|
1104
|
+
* the menu asserted here is the row's, and the field's own must be absent.
|
|
1105
|
+
*/
|
|
1106
|
+
export const FieldMenuStaysInTheToolbar: Story = {
|
|
1107
|
+
parameters: {
|
|
1108
|
+
docs: {
|
|
1109
|
+
description: {
|
|
1110
|
+
story:
|
|
1111
|
+
"Accepts the offer on a compact row whose field also has templates. The row shows ONE More menu — the field publishes its items into the row's ⋮ rather than drawing its own — and that menu sits in the shell's toolbar row, level with Undo, not part-way down the editor.",
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
},
|
|
1115
|
+
render: () => {
|
|
1116
|
+
const [value, setValue] = useState<any>({});
|
|
1117
|
+
return (
|
|
1118
|
+
<FormEngine
|
|
1119
|
+
name='dpqlOfferToolbarForm'
|
|
1120
|
+
compact
|
|
1121
|
+
flat
|
|
1122
|
+
transparent
|
|
1123
|
+
padded={false}
|
|
1124
|
+
showTypeToggle={false}
|
|
1125
|
+
size='small'
|
|
1126
|
+
options={
|
|
1127
|
+
{
|
|
1128
|
+
expected: {
|
|
1129
|
+
type: 'string',
|
|
1130
|
+
ui_type: 'string',
|
|
1131
|
+
display_name: 'Value',
|
|
1132
|
+
preselected: true,
|
|
1133
|
+
supports_expressions: true,
|
|
1134
|
+
supports_templates: true,
|
|
1135
|
+
expressions: mockExpressions,
|
|
1136
|
+
templates: {
|
|
1137
|
+
items: [
|
|
1138
|
+
{
|
|
1139
|
+
label: 'Values this case captures',
|
|
1140
|
+
items: [{ label: 'Order status', value: '$.order.status', badge: 'string' }],
|
|
1141
|
+
},
|
|
1142
|
+
],
|
|
1143
|
+
},
|
|
1144
|
+
},
|
|
1145
|
+
} as any
|
|
1146
|
+
}
|
|
1147
|
+
value={value}
|
|
1148
|
+
onChange={(_n, v) => setValue(v)}
|
|
1149
|
+
/>
|
|
1150
|
+
);
|
|
1151
|
+
},
|
|
1152
|
+
async play({ canvasElement }) {
|
|
1153
|
+
await openCompactRow(canvasElement);
|
|
1154
|
+
await acceptTypedExpression(canvasElement);
|
|
1155
|
+
|
|
1156
|
+
// One menu per control: the row's ⋮, not a second one drawn by the field.
|
|
1157
|
+
expect(canvasElement.querySelector('.template-more')).toBeNull();
|
|
1158
|
+
const menus = canvasElement.querySelectorAll('.options-readfirst-more');
|
|
1159
|
+
expect(menus).toHaveLength(1);
|
|
1160
|
+
const menu = menus[0] as HTMLElement;
|
|
1161
|
+
|
|
1162
|
+
// The toolbar row is the shell's first line — the Visual/Text toggle.
|
|
1163
|
+
const toggle = within(canvasElement).getByText('Text').closest('button') as HTMLElement;
|
|
1164
|
+
const toolbar = toggle.getBoundingClientRect();
|
|
1165
|
+
const control = menu.getBoundingClientRect();
|
|
1166
|
+
|
|
1167
|
+
/* Reported as numbers: "the menu is in the wrong place" is not something a
|
|
1168
|
+
CI log can show, and the two rectangles are the whole story. */
|
|
1169
|
+
const where = `toolbar ${Math.round(toolbar.top)}–${Math.round(
|
|
1170
|
+
toolbar.bottom
|
|
1171
|
+
)}, menu ${Math.round(control.top)}–${Math.round(control.bottom)}`;
|
|
1172
|
+
|
|
1173
|
+
// Level with the toolbar: the menu's own centre falls inside that row.
|
|
1174
|
+
const centre = control.top + control.height / 2;
|
|
1175
|
+
expect(
|
|
1176
|
+
centre >= toolbar.top && centre <= toolbar.bottom,
|
|
1177
|
+
`the field's More menu should sit in the toolbar row — ${where}`
|
|
1178
|
+
).toBe(true);
|
|
1179
|
+
},
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* The Text editor seeds from an AST that arrives AFTER the mode flips.
|
|
1184
|
+
*
|
|
1185
|
+
* `TemplateField` passes the shell its OWN `value` prop, and on the render
|
|
1186
|
+
* that flips the field into expression mode that prop is still the STRING the
|
|
1187
|
+
* author typed — the parsed AST only comes back through the host form one
|
|
1188
|
+
* render later. So the shell mounts in Text mode with nothing to serialize,
|
|
1189
|
+
* and an effect that runs on the mode alone has already had its only turn: the
|
|
1190
|
+
* author gets an empty editor with a Preview of an expression sitting beside
|
|
1191
|
+
* it, which reads as the text having been thrown away.
|
|
1192
|
+
*
|
|
1193
|
+
* Modelled here rather than live because it is a pure ordering problem — no
|
|
1194
|
+
* instance, no offer, no host form. The late arrival is the whole test.
|
|
1195
|
+
*/
|
|
1196
|
+
export const TextModeSeedsWhenAstArrivesLate: Story = {
|
|
1197
|
+
parameters: {
|
|
1198
|
+
docs: {
|
|
1199
|
+
description: {
|
|
1200
|
+
story:
|
|
1201
|
+
"Mounts the shell in Text mode with a value that is not yet an AST, then delivers the AST a moment later — the DPQL editor must seed from it rather than stay empty.",
|
|
1202
|
+
},
|
|
1203
|
+
},
|
|
1204
|
+
},
|
|
1205
|
+
render: () => {
|
|
1206
|
+
const [value, setValue] = useState<IExpression>({
|
|
1207
|
+
is_expression: true,
|
|
1208
|
+
// What `TemplateField` actually holds at that moment: the typed text.
|
|
1209
|
+
value: '1 + 2' as unknown as IExpression['value'],
|
|
1210
|
+
});
|
|
1211
|
+
useEffect(() => {
|
|
1212
|
+
const timer = setTimeout(() => setValue(SAMPLE), 100);
|
|
1213
|
+
return () => clearTimeout(timer);
|
|
1214
|
+
}, []);
|
|
1215
|
+
return (
|
|
1216
|
+
<ExpressionField
|
|
1217
|
+
value={value}
|
|
1218
|
+
onChange={noopChange}
|
|
1219
|
+
defaultMode='text'
|
|
1220
|
+
expressions={mockExpressions}
|
|
1221
|
+
/>
|
|
1222
|
+
);
|
|
1223
|
+
},
|
|
1224
|
+
async play({ canvasElement }) {
|
|
1225
|
+
const editable = (await waitFor(
|
|
1226
|
+
() => {
|
|
1227
|
+
const el = canvasElement.querySelector('[contenteditable="true"]');
|
|
1228
|
+
if (!el) throw new Error('editor not ready');
|
|
1229
|
+
return el as HTMLElement;
|
|
1230
|
+
},
|
|
1231
|
+
{ timeout: 10000 }
|
|
1232
|
+
)) as HTMLElement;
|
|
1233
|
+
|
|
1234
|
+
// The AST arrives late; the editor must catch up with it.
|
|
1235
|
+
await waitFor(
|
|
1236
|
+
() => {
|
|
1237
|
+
expect(editable.textContent).toMatch(/local:?\s*name/);
|
|
1238
|
+
expect(editable.textContent).toContain('John');
|
|
1239
|
+
},
|
|
1240
|
+
{ timeout: 10000 }
|
|
1241
|
+
);
|
|
1242
|
+
await waitForLspIdle(canvasElement);
|
|
1243
|
+
},
|
|
1244
|
+
};
|
|
1245
|
+
|
|
1246
|
+
/**
|
|
1247
|
+
* The Text view offers template completions.
|
|
1248
|
+
*
|
|
1249
|
+
* Templates are how an author names a value the surrounding interface already
|
|
1250
|
+
* has, and the Text view is a DPQL editor like any other: typing `$` asks the
|
|
1251
|
+
* language server, in position, and shows what it answers. Nothing about
|
|
1252
|
+
* mounting that editor inside the expression shell changes it — the shell
|
|
1253
|
+
* passes no template list of its own, because there is none to pass.
|
|
1254
|
+
*
|
|
1255
|
+
* Asserted here because it had never been asserted anywhere: the shell's Text
|
|
1256
|
+
* view had only ever been exercised for parse, serialize and type analysis, so
|
|
1257
|
+
* "does `$` still work in there?" was an open question with no cost to
|
|
1258
|
+
* answering. What this covers is the CLIENT half — that the shell surfaces
|
|
1259
|
+
* what the server offers. Which contexts the live Qorus server offers is its
|
|
1260
|
+
* own question, and this cannot answer it.
|
|
1261
|
+
*/
|
|
1262
|
+
export const TextModeOffersTemplateCompletions: Story = {
|
|
1263
|
+
args: {
|
|
1264
|
+
value: { is_expression: true, value: { args: [] } },
|
|
1265
|
+
defaultMode: 'text',
|
|
1266
|
+
},
|
|
1267
|
+
parameters: {
|
|
1268
|
+
docs: {
|
|
1269
|
+
description: {
|
|
1270
|
+
story:
|
|
1271
|
+
'Types `$` into the shell\'s Text view — the DPQL editor asks the language server in position and the template namespaces ($data:, $config:, …) open in the completion dropdown.',
|
|
1272
|
+
},
|
|
1273
|
+
},
|
|
1274
|
+
},
|
|
1275
|
+
async play({ canvasElement }) {
|
|
1276
|
+
const editable = (await waitFor(
|
|
1277
|
+
() => {
|
|
1278
|
+
const el = canvasElement.querySelector('[contenteditable="true"]');
|
|
1279
|
+
if (!el) throw new Error('editor not ready');
|
|
1280
|
+
return el as HTMLElement;
|
|
1281
|
+
},
|
|
1282
|
+
{ timeout: 10000 }
|
|
1283
|
+
)) as HTMLElement;
|
|
1284
|
+
|
|
1285
|
+
// The editor mounting is not the language server being ready, and a `$`
|
|
1286
|
+
// typed before the socket is up asks nothing at all.
|
|
1287
|
+
await waitForLspIdle(canvasElement);
|
|
1288
|
+
await userEvent.click(editable);
|
|
1289
|
+
await userEvent.type(editable, '$');
|
|
1290
|
+
|
|
1291
|
+
// The dropdown is portalled, so it is looked for in the document rather
|
|
1292
|
+
// than in the canvas.
|
|
1293
|
+
await waitFor(
|
|
1294
|
+
() => {
|
|
1295
|
+
const dropdown = document.querySelector('.reqore-menu');
|
|
1296
|
+
expect(dropdown).not.toBeNull();
|
|
1297
|
+
expect(dropdown!.textContent).toContain('$data:');
|
|
1298
|
+
expect(dropdown!.textContent).toContain('$config:');
|
|
1299
|
+
},
|
|
1300
|
+
{ timeout: 20000 }
|
|
1301
|
+
);
|
|
1302
|
+
},
|
|
1303
|
+
};
|
|
1304
|
+
|
|
527
1305
|
/**
|
|
528
1306
|
* LIVE — fetches the real expression catalogue from the configured Qorus
|
|
529
1307
|
* instance (no `expressions` override). This is the true apples-to-apples
|
|
@@ -583,9 +1361,9 @@ export const Live: Story = {
|
|
|
583
1361
|
|
|
584
1362
|
/**
|
|
585
1363
|
* LIVE — server-side rendering over the LSP (`dpql/renderExpression`). Text
|
|
586
|
-
* mode against the real instance: the live "
|
|
1364
|
+
* mode against the real instance: the live "Preview" box should show the
|
|
587
1365
|
* server rendering — `"test".startsWith("t", true)` — not the DPQL form
|
|
588
|
-
* (`"test" startsWith "t"`)
|
|
1366
|
+
* (`"test" startsWith "t"`). Compare with
|
|
589
1367
|
* qorus-ide's Explain (storybook :6007) for the same AST. Prereq: same as
|
|
590
1368
|
* `Live`; note the LSP WebSocket is NOT CORS-blocked, unlike REST fetches
|
|
591
1369
|
* from storybook to localhost:8012.
|
|
@@ -597,7 +1375,7 @@ export const LiveExplain: Story = {
|
|
|
597
1375
|
docs: {
|
|
598
1376
|
description: {
|
|
599
1377
|
story:
|
|
600
|
-
'Renders ExpressionField in Text mode against a live Qorus instance — the "
|
|
1378
|
+
'Renders ExpressionField in Text mode against a live Qorus instance — the "Preview" box shows the server\'s readable rendering from dpql/renderExpression, `"test".startsWith("t", true)`, beside the DPQL it was written as, `"test" startsWith "t"`.',
|
|
601
1379
|
},
|
|
602
1380
|
},
|
|
603
1381
|
},
|