@teleporthq/teleport-plugin-common 0.44.0-alpha.0 → 0.44.0
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/__tests__/builders/html-builders.ts +9 -2
- package/__tests__/builders/style-builders.ts +23 -0
- package/__tests__/node-handlers/node-to-html/index.ts +2 -0
- package/__tests__/node-handlers/node-to-jsx/attribute-value-mapper.ts +77 -0
- package/__tests__/node-handlers/node-to-jsx/calendar-events-reviver.ts +176 -0
- package/__tests__/node-handlers/node-to-jsx/cms-list-repeater-ecommerce-source.ts +79 -0
- package/__tests__/node-handlers/node-to-jsx/cms-list-repeater-loading.ts +122 -0
- package/__tests__/node-handlers/node-to-jsx/index.ts +719 -3
- package/__tests__/node-handlers/node-to-jsx/url-search-params-expression.ts +59 -0
- package/__tests__/node-handlers/node-to-jsx/utils.ts +602 -3
- package/__tests__/utils/ast-utils.ts +188 -0
- package/__tests__/utils/content-property-sanitiser.ts +156 -0
- package/__tests__/utils/page-seeds-state-from-data-source.ts +70 -0
- package/__tests__/utils/parse-string-with-template-expressions.ts +82 -0
- package/__tests__/utils/resource-auth-scheme.ts +55 -0
- package/__tests__/utils/route-utils.ts +118 -0
- package/__tests__/utils/style-utils.ts +59 -0
- package/__tests__/utils/template-expression-balance.ts +83 -0
- package/__tests__/utils/url-query-writer.ts +239 -0
- package/__tests__/utils/url-search-param-sync.ts +138 -0
- package/__tests__/utils/url-search-params.ts +48 -0
- package/dist/cjs/builders/ast-builders.d.ts +2 -0
- package/dist/cjs/builders/ast-builders.d.ts.map +1 -1
- package/dist/cjs/builders/ast-builders.js +41 -3
- package/dist/cjs/builders/ast-builders.js.map +1 -1
- package/dist/cjs/builders/hast-builders.d.ts +1 -0
- package/dist/cjs/builders/hast-builders.d.ts.map +1 -1
- package/dist/cjs/builders/hast-builders.js +11 -1
- package/dist/cjs/builders/hast-builders.js.map +1 -1
- package/dist/cjs/builders/style-builders.d.ts +8 -2
- package/dist/cjs/builders/style-builders.d.ts.map +1 -1
- package/dist/cjs/builders/style-builders.js +65 -19
- package/dist/cjs/builders/style-builders.js.map +1 -1
- package/dist/cjs/index.d.ts +5 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +9 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/node-handlers/node-to-html/constants.d.ts.map +1 -1
- package/dist/cjs/node-handlers/node-to-html/constants.js +2 -0
- package/dist/cjs/node-handlers/node-to-html/constants.js.map +1 -1
- package/dist/cjs/node-handlers/node-to-html/index.d.ts.map +1 -1
- package/dist/cjs/node-handlers/node-to-html/index.js +69 -1
- package/dist/cjs/node-handlers/node-to-html/index.js.map +1 -1
- package/dist/cjs/node-handlers/node-to-html/types.d.ts +5 -1
- package/dist/cjs/node-handlers/node-to-html/types.d.ts.map +1 -1
- package/dist/cjs/node-handlers/node-to-html/utils.d.ts +2 -1
- package/dist/cjs/node-handlers/node-to-html/utils.d.ts.map +1 -1
- package/dist/cjs/node-handlers/node-to-html/utils.js +174 -13
- package/dist/cjs/node-handlers/node-to-html/utils.js.map +1 -1
- package/dist/cjs/node-handlers/node-to-jsx/index.d.ts.map +1 -1
- package/dist/cjs/node-handlers/node-to-jsx/index.js +1461 -66
- package/dist/cjs/node-handlers/node-to-jsx/index.js.map +1 -1
- package/dist/cjs/node-handlers/node-to-jsx/types.d.ts +60 -6
- package/dist/cjs/node-handlers/node-to-jsx/types.d.ts.map +1 -1
- package/dist/cjs/node-handlers/node-to-jsx/utils.d.ts +73 -7
- package/dist/cjs/node-handlers/node-to-jsx/utils.d.ts.map +1 -1
- package/dist/cjs/node-handlers/node-to-jsx/utils.js +864 -71
- package/dist/cjs/node-handlers/node-to-jsx/utils.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils/ast-utils.d.ts +142 -11
- package/dist/cjs/utils/ast-utils.d.ts.map +1 -1
- package/dist/cjs/utils/ast-utils.js +931 -75
- package/dist/cjs/utils/ast-utils.js.map +1 -1
- package/dist/cjs/utils/hast-utils.d.ts +8 -6
- package/dist/cjs/utils/hast-utils.d.ts.map +1 -1
- package/dist/cjs/utils/hast-utils.js +28 -4
- package/dist/cjs/utils/hast-utils.js.map +1 -1
- package/dist/cjs/utils/route-utils.d.ts +90 -0
- package/dist/cjs/utils/route-utils.d.ts.map +1 -0
- package/dist/cjs/utils/route-utils.js +152 -0
- package/dist/cjs/utils/route-utils.js.map +1 -0
- package/dist/cjs/utils/style-utils.d.ts +1 -0
- package/dist/cjs/utils/style-utils.d.ts.map +1 -1
- package/dist/cjs/utils/style-utils.js +181 -2
- package/dist/cjs/utils/style-utils.js.map +1 -1
- package/dist/cjs/utils/template-expression-balance.d.ts +58 -0
- package/dist/cjs/utils/template-expression-balance.d.ts.map +1 -0
- package/dist/cjs/utils/template-expression-balance.js +181 -0
- package/dist/cjs/utils/template-expression-balance.js.map +1 -0
- package/dist/cjs/utils/types.d.ts +3 -0
- package/dist/cjs/utils/types.d.ts.map +1 -0
- package/dist/cjs/utils/types.js +3 -0
- package/dist/cjs/utils/types.js.map +1 -0
- package/dist/cjs/utils/url-query-writer.d.ts +123 -0
- package/dist/cjs/utils/url-query-writer.d.ts.map +1 -0
- package/dist/cjs/utils/url-query-writer.js +260 -0
- package/dist/cjs/utils/url-query-writer.js.map +1 -0
- package/dist/cjs/utils/url-search-param-sync.d.ts +4 -0
- package/dist/cjs/utils/url-search-param-sync.d.ts.map +1 -0
- package/dist/cjs/utils/url-search-param-sync.js +241 -0
- package/dist/cjs/utils/url-search-param-sync.js.map +1 -0
- package/dist/cjs/utils/url-search-params.d.ts +48 -0
- package/dist/cjs/utils/url-search-params.d.ts.map +1 -0
- package/dist/cjs/utils/url-search-params.js +109 -0
- package/dist/cjs/utils/url-search-params.js.map +1 -0
- package/dist/esm/builders/ast-builders.d.ts +2 -0
- package/dist/esm/builders/ast-builders.d.ts.map +1 -1
- package/dist/esm/builders/ast-builders.js +40 -4
- package/dist/esm/builders/ast-builders.js.map +1 -1
- package/dist/esm/builders/hast-builders.d.ts +1 -0
- package/dist/esm/builders/hast-builders.d.ts.map +1 -1
- package/dist/esm/builders/hast-builders.js +9 -0
- package/dist/esm/builders/hast-builders.js.map +1 -1
- package/dist/esm/builders/style-builders.d.ts +8 -2
- package/dist/esm/builders/style-builders.d.ts.map +1 -1
- package/dist/esm/builders/style-builders.js +63 -18
- package/dist/esm/builders/style-builders.js.map +1 -1
- package/dist/esm/index.d.ts +5 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +5 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/node-handlers/node-to-html/constants.d.ts.map +1 -1
- package/dist/esm/node-handlers/node-to-html/constants.js +2 -0
- package/dist/esm/node-handlers/node-to-html/constants.js.map +1 -1
- package/dist/esm/node-handlers/node-to-html/index.d.ts.map +1 -1
- package/dist/esm/node-handlers/node-to-html/index.js +71 -3
- package/dist/esm/node-handlers/node-to-html/index.js.map +1 -1
- package/dist/esm/node-handlers/node-to-html/types.d.ts +5 -1
- package/dist/esm/node-handlers/node-to-html/types.d.ts.map +1 -1
- package/dist/esm/node-handlers/node-to-html/utils.d.ts +2 -1
- package/dist/esm/node-handlers/node-to-html/utils.d.ts.map +1 -1
- package/dist/esm/node-handlers/node-to-html/utils.js +170 -13
- package/dist/esm/node-handlers/node-to-html/utils.js.map +1 -1
- package/dist/esm/node-handlers/node-to-jsx/index.d.ts.map +1 -1
- package/dist/esm/node-handlers/node-to-jsx/index.js +1460 -68
- package/dist/esm/node-handlers/node-to-jsx/index.js.map +1 -1
- package/dist/esm/node-handlers/node-to-jsx/types.d.ts +60 -6
- package/dist/esm/node-handlers/node-to-jsx/types.d.ts.map +1 -1
- package/dist/esm/node-handlers/node-to-jsx/utils.d.ts +73 -7
- package/dist/esm/node-handlers/node-to-jsx/utils.d.ts.map +1 -1
- package/dist/esm/node-handlers/node-to-jsx/utils.js +857 -69
- package/dist/esm/node-handlers/node-to-jsx/utils.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils/ast-utils.d.ts +142 -11
- package/dist/esm/utils/ast-utils.d.ts.map +1 -1
- package/dist/esm/utils/ast-utils.js +911 -75
- package/dist/esm/utils/ast-utils.js.map +1 -1
- package/dist/esm/utils/hast-utils.d.ts +8 -6
- package/dist/esm/utils/hast-utils.d.ts.map +1 -1
- package/dist/esm/utils/hast-utils.js +26 -3
- package/dist/esm/utils/hast-utils.js.map +1 -1
- package/dist/esm/utils/route-utils.d.ts +90 -0
- package/dist/esm/utils/route-utils.d.ts.map +1 -0
- package/dist/esm/utils/route-utils.js +144 -0
- package/dist/esm/utils/route-utils.js.map +1 -0
- package/dist/esm/utils/style-utils.d.ts +1 -0
- package/dist/esm/utils/style-utils.d.ts.map +1 -1
- package/dist/esm/utils/style-utils.js +179 -1
- package/dist/esm/utils/style-utils.js.map +1 -1
- package/dist/esm/utils/template-expression-balance.d.ts +58 -0
- package/dist/esm/utils/template-expression-balance.d.ts.map +1 -0
- package/dist/esm/utils/template-expression-balance.js +174 -0
- package/dist/esm/utils/template-expression-balance.js.map +1 -0
- package/dist/esm/utils/types.d.ts +3 -0
- package/dist/esm/utils/types.d.ts.map +1 -0
- package/dist/esm/utils/types.js +2 -0
- package/dist/esm/utils/types.js.map +1 -0
- package/dist/esm/utils/url-query-writer.d.ts +123 -0
- package/dist/esm/utils/url-query-writer.d.ts.map +1 -0
- package/dist/esm/utils/url-query-writer.js +229 -0
- package/dist/esm/utils/url-query-writer.js.map +1 -0
- package/dist/esm/utils/url-search-param-sync.d.ts +4 -0
- package/dist/esm/utils/url-search-param-sync.d.ts.map +1 -0
- package/dist/esm/utils/url-search-param-sync.js +213 -0
- package/dist/esm/utils/url-search-param-sync.js.map +1 -0
- package/dist/esm/utils/url-search-params.d.ts +48 -0
- package/dist/esm/utils/url-search-params.d.ts.map +1 -0
- package/dist/esm/utils/url-search-params.js +81 -0
- package/dist/esm/utils/url-search-params.js.map +1 -0
- package/package.json +4 -4
- package/src/builders/ast-builders.ts +80 -3
- package/src/builders/hast-builders.ts +11 -0
- package/src/builders/style-builders.ts +107 -27
- package/src/index.ts +18 -1
- package/src/node-handlers/node-to-html/constants.ts +2 -0
- package/src/node-handlers/node-to-html/index.ts +126 -6
- package/src/node-handlers/node-to-html/types.ts +6 -0
- package/src/node-handlers/node-to-html/utils.ts +228 -14
- package/src/node-handlers/node-to-jsx/index.ts +2219 -111
- package/src/node-handlers/node-to-jsx/types.ts +75 -32
- package/src/node-handlers/node-to-jsx/utils.ts +1142 -84
- package/src/utils/ast-utils.ts +1223 -127
- package/src/utils/hast-utils.ts +37 -9
- package/src/utils/route-utils.ts +168 -0
- package/src/utils/style-utils.ts +183 -1
- package/src/utils/template-expression-balance.ts +196 -0
- package/src/utils/types.ts +25 -0
- package/src/utils/url-query-writer.ts +318 -0
- package/src/utils/url-search-param-sync.ts +303 -0
- package/src/utils/url-search-params.ts +122 -0
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import * as types from '@babel/types'
|
|
2
2
|
import componentUIDLSample from '../../../../../examples/test-samples/component-sample.json'
|
|
3
3
|
import generateJSXSyntax from '../../../src/node-handlers/node-to-jsx'
|
|
4
|
+
import generate from '@babel/generator'
|
|
4
5
|
|
|
5
|
-
import { slotNode, elementNode, staticNode } from '@teleporthq/teleport-uidl-builders'
|
|
6
|
+
import { slotNode, elementNode, staticNode, dynamicNode } from '@teleporthq/teleport-uidl-builders'
|
|
6
7
|
import {
|
|
7
8
|
JSXGenerationParams,
|
|
8
9
|
JSXGenerationOptions,
|
|
9
10
|
} from '../../../src/node-handlers/node-to-jsx/types'
|
|
10
|
-
import { ComponentUIDL } from '@teleporthq/teleport-types'
|
|
11
|
+
import { ComponentUIDL, UIDLNode } from '@teleporthq/teleport-types'
|
|
11
12
|
|
|
12
13
|
const uidl = componentUIDLSample as ComponentUIDL
|
|
13
14
|
|
|
@@ -16,7 +17,14 @@ describe('generateJSXSyntax', () => {
|
|
|
16
17
|
dependencies: {},
|
|
17
18
|
propDefinitions: uidl.propDefinitions || {},
|
|
18
19
|
stateDefinitions: uidl.stateDefinitions || {},
|
|
20
|
+
globalStateDefinitions: {},
|
|
19
21
|
nodesLookup: {},
|
|
22
|
+
windowImports: {},
|
|
23
|
+
localeReferences: [],
|
|
24
|
+
localeAttributeReferences: [],
|
|
25
|
+
globalReferences: [],
|
|
26
|
+
globalStateReferences: [],
|
|
27
|
+
hoistedConstants: [],
|
|
20
28
|
}
|
|
21
29
|
|
|
22
30
|
const options: JSXGenerationOptions = {
|
|
@@ -24,7 +32,6 @@ describe('generateJSXSyntax', () => {
|
|
|
24
32
|
prop: 'props',
|
|
25
33
|
state: '',
|
|
26
34
|
local: '',
|
|
27
|
-
ctx: '',
|
|
28
35
|
},
|
|
29
36
|
}
|
|
30
37
|
|
|
@@ -39,6 +46,38 @@ describe('generateJSXSyntax', () => {
|
|
|
39
46
|
})
|
|
40
47
|
})
|
|
41
48
|
|
|
49
|
+
describe('dynamic style values', () => {
|
|
50
|
+
it('keeps a custom property key verbatim in the inline style object', () => {
|
|
51
|
+
// React writes `--*` keys through `style.setProperty`; the camel rule
|
|
52
|
+
// turned `--tq-panel-display` into `-TqPanelDisplay`, which React neither
|
|
53
|
+
// renders nor updates — the state-driven panel never opened.
|
|
54
|
+
const node = elementNode('container', {}, [], undefined, {
|
|
55
|
+
'--tq-panel-display': dynamicNode('state', 'panelDisplay'),
|
|
56
|
+
'background-color': dynamicNode('state', 'panelColor'),
|
|
57
|
+
})
|
|
58
|
+
const result = generateJSXSyntax(
|
|
59
|
+
node,
|
|
60
|
+
{
|
|
61
|
+
...params,
|
|
62
|
+
stateDefinitions: {
|
|
63
|
+
panelDisplay: { type: 'string', defaultValue: 'none' },
|
|
64
|
+
panelColor: { type: 'string', defaultValue: 'red' },
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
options
|
|
68
|
+
)
|
|
69
|
+
const styleAttr = (result as types.JSXElement).openingElement.attributes.find(
|
|
70
|
+
(attr) => attr.type === 'JSXAttribute' && attr.name.name === 'style'
|
|
71
|
+
) as types.JSXAttribute
|
|
72
|
+
const object = (styleAttr.value as types.JSXExpressionContainer)
|
|
73
|
+
.expression as types.ObjectExpression
|
|
74
|
+
const keys = object.properties.map(
|
|
75
|
+
(prop) => ((prop as types.ObjectProperty).key as types.StringLiteral).value
|
|
76
|
+
)
|
|
77
|
+
expect(keys).toEqual(['--tq-panel-display', 'backgroundColor'])
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
42
81
|
describe('slot node', () => {
|
|
43
82
|
it('returns a props.children expression', () => {
|
|
44
83
|
const node = elementNode('container', {}, [slotNode()])
|
|
@@ -104,4 +143,681 @@ describe('generateJSXSyntax', () => {
|
|
|
104
143
|
expect((nameAttr.value as types.StringLiteral).value).toBe('hole')
|
|
105
144
|
})
|
|
106
145
|
})
|
|
146
|
+
|
|
147
|
+
describe('markdown-node with dynamic/expr children (CMS rich text)', () => {
|
|
148
|
+
const buildParams = (): JSXGenerationParams => ({
|
|
149
|
+
dependencies: {},
|
|
150
|
+
propDefinitions: {},
|
|
151
|
+
stateDefinitions: {},
|
|
152
|
+
globalStateDefinitions: {},
|
|
153
|
+
nodesLookup: {},
|
|
154
|
+
windowImports: {},
|
|
155
|
+
localeReferences: [],
|
|
156
|
+
localeAttributeReferences: [],
|
|
157
|
+
globalReferences: [],
|
|
158
|
+
globalStateReferences: [],
|
|
159
|
+
hoistedConstants: [],
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Asserts the element is a <Markdown>{<expr> || ''}</Markdown> tag from markdown-to-jsx,
|
|
163
|
+
// which renders both markdown syntax and embedded HTML.
|
|
164
|
+
const expectMarkdownTag = (result: types.JSXElement, localParams: JSXGenerationParams) => {
|
|
165
|
+
expect((result.openingElement.name as types.JSXIdentifier).name).toBe('Markdown')
|
|
166
|
+
|
|
167
|
+
const container = result.children[0] as types.JSXExpressionContainer
|
|
168
|
+
expect(container.type).toBe('JSXExpressionContainer')
|
|
169
|
+
// dynamic content is guarded with `<expr> || ''` so a nullish value renders empty
|
|
170
|
+
expect((container.expression as types.LogicalExpression).operator).toBe('||')
|
|
171
|
+
|
|
172
|
+
expect(localParams.dependencies.Markdown).toEqual({
|
|
173
|
+
type: 'package',
|
|
174
|
+
path: 'markdown-to-jsx',
|
|
175
|
+
version: '7.7.12',
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
it('renders an expr child through the markdown-to-jsx <Markdown> component', () => {
|
|
180
|
+
const localParams = buildParams()
|
|
181
|
+
const node = elementNode('markdown-node', {}, [
|
|
182
|
+
{ type: 'expr', content: 'props.cmsRichtext' } as UIDLNode,
|
|
183
|
+
])
|
|
184
|
+
|
|
185
|
+
const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
|
|
186
|
+
|
|
187
|
+
expectMarkdownTag(result, localParams)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('renders a dynamic child through the markdown-to-jsx <Markdown> component', () => {
|
|
191
|
+
const localParams = buildParams()
|
|
192
|
+
const node = elementNode('markdown-node', {}, [dynamicNode('prop', 'cmsRichtext')])
|
|
193
|
+
|
|
194
|
+
const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
|
|
195
|
+
|
|
196
|
+
expectMarkdownTag(result, localParams)
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('leaves a non-markdown div with an expr child as a plain value', () => {
|
|
200
|
+
const localParams = buildParams()
|
|
201
|
+
const node = elementNode('container', {}, [
|
|
202
|
+
{ type: 'expr', content: 'props.cmsRichtext' } as UIDLNode,
|
|
203
|
+
])
|
|
204
|
+
|
|
205
|
+
const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
|
|
206
|
+
|
|
207
|
+
// generic expression handling wraps in a typeof/JSON.stringify guard, no markdown render
|
|
208
|
+
const container = result.children[0] as types.JSXExpressionContainer
|
|
209
|
+
expect(container.expression.type).toBe('ConditionalExpression')
|
|
210
|
+
expect((result.openingElement.name as types.JSXIdentifier).name).not.toBe('Markdown')
|
|
211
|
+
expect(localParams.dependencies.Markdown).toBeUndefined()
|
|
212
|
+
})
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
describe('renderingConditions on elements', () => {
|
|
216
|
+
it('wraps the element with a logical `&&` when renderingConditions are set', () => {
|
|
217
|
+
const ownerContainer = elementNode('container', {}, [])
|
|
218
|
+
ownerContainer.content.renderingConditions = {
|
|
219
|
+
reference: {
|
|
220
|
+
type: 'dynamic',
|
|
221
|
+
content: {
|
|
222
|
+
referenceType: 'state',
|
|
223
|
+
id: 'profileViewer',
|
|
224
|
+
refPath: ['currentUserId'],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
condition: {
|
|
228
|
+
conditions: [
|
|
229
|
+
{
|
|
230
|
+
operation: '===',
|
|
231
|
+
operand: {
|
|
232
|
+
type: 'dynamic',
|
|
233
|
+
content: {
|
|
234
|
+
referenceType: 'local',
|
|
235
|
+
id: 'id',
|
|
236
|
+
refPath: ['id'],
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
},
|
|
242
|
+
} as never
|
|
243
|
+
|
|
244
|
+
const wrappingParent = elementNode('container', {}, [ownerContainer])
|
|
245
|
+
const detailsParams: JSXGenerationParams = {
|
|
246
|
+
...params,
|
|
247
|
+
globalReferences: [],
|
|
248
|
+
detailsPageExposeAsName: 'User',
|
|
249
|
+
}
|
|
250
|
+
const result = generateJSXSyntax(wrappingParent, detailsParams, options)
|
|
251
|
+
|
|
252
|
+
const expressionChild = result.children[0] as types.JSXExpressionContainer
|
|
253
|
+
expect(expressionChild.type).toBe('JSXExpressionContainer')
|
|
254
|
+
|
|
255
|
+
const logical = expressionChild.expression as types.LogicalExpression
|
|
256
|
+
expect(logical.type).toBe('LogicalExpression')
|
|
257
|
+
expect(logical.operator).toBe('&&')
|
|
258
|
+
|
|
259
|
+
// The wrapped content is the original <container/>.
|
|
260
|
+
const wrappedContent = logical.right as types.JSXElement
|
|
261
|
+
expect(wrappedContent.type).toBe('JSXElement')
|
|
262
|
+
expect((wrappedContent.openingElement.name as types.JSXIdentifier).name).toBe('container')
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
const buildTwoReferenceConditions = (matchingCriteria: string) =>
|
|
266
|
+
({
|
|
267
|
+
reference: {
|
|
268
|
+
type: 'dynamic',
|
|
269
|
+
content: { referenceType: 'state', id: 'newsletterOfferVisible' },
|
|
270
|
+
},
|
|
271
|
+
condition: {
|
|
272
|
+
conditions: [
|
|
273
|
+
{ operation: '===', operand: true },
|
|
274
|
+
{
|
|
275
|
+
operation: '===',
|
|
276
|
+
operand: false,
|
|
277
|
+
reference: {
|
|
278
|
+
type: 'dynamic',
|
|
279
|
+
content: { referenceType: 'state', id: 'userIsLoggedIn' },
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
matchingCriteria,
|
|
284
|
+
},
|
|
285
|
+
} as never)
|
|
286
|
+
|
|
287
|
+
it('compares each entry against its own reference when one is set (AND chain)', () => {
|
|
288
|
+
const ownerContainer = elementNode('container', {}, [])
|
|
289
|
+
ownerContainer.content.renderingConditions = buildTwoReferenceConditions('all')
|
|
290
|
+
|
|
291
|
+
const wrappingParent = elementNode('container', {}, [ownerContainer])
|
|
292
|
+
const twoStateParams: JSXGenerationParams = {
|
|
293
|
+
...params,
|
|
294
|
+
stateDefinitions: {
|
|
295
|
+
newsletterOfferVisible: { type: 'boolean', defaultValue: true },
|
|
296
|
+
userIsLoggedIn: { type: 'boolean', defaultValue: false },
|
|
297
|
+
},
|
|
298
|
+
globalReferences: [],
|
|
299
|
+
globalStateReferences: [],
|
|
300
|
+
}
|
|
301
|
+
const result = generateJSXSyntax(wrappingParent, twoStateParams, options)
|
|
302
|
+
|
|
303
|
+
const expressionChild = result.children[0] as types.JSXExpressionContainer
|
|
304
|
+
const outerLogical = expressionChild.expression as types.LogicalExpression
|
|
305
|
+
expect(outerLogical.operator).toBe('&&')
|
|
306
|
+
|
|
307
|
+
// Left side is the chained condition pair, joined with `&&` for 'all'.
|
|
308
|
+
const conditionChain = outerLogical.left as types.LogicalExpression
|
|
309
|
+
expect(conditionChain.type).toBe('LogicalExpression')
|
|
310
|
+
expect(conditionChain.operator).toBe('&&')
|
|
311
|
+
|
|
312
|
+
// `=== true` compiles to the bare identifier, `=== false` to `!identifier`
|
|
313
|
+
// — i.e. `newsletterOfferVisible && !userIsLoggedIn`, the wild-log case.
|
|
314
|
+
const firstComparison = conditionChain.left as types.Identifier
|
|
315
|
+
const secondComparison = conditionChain.right as types.UnaryExpression
|
|
316
|
+
expect(firstComparison.name).toBe('newsletterOfferVisible')
|
|
317
|
+
expect(secondComparison.operator).toBe('!')
|
|
318
|
+
expect((secondComparison.argument as types.Identifier).name).toBe('userIsLoggedIn')
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('chains mixed-reference entries with `||` when matchingCriteria is not "all"', () => {
|
|
322
|
+
const ownerContainer = elementNode('container', {}, [])
|
|
323
|
+
ownerContainer.content.renderingConditions = buildTwoReferenceConditions('||')
|
|
324
|
+
|
|
325
|
+
const wrappingParent = elementNode('container', {}, [ownerContainer])
|
|
326
|
+
const twoStateParams: JSXGenerationParams = {
|
|
327
|
+
...params,
|
|
328
|
+
stateDefinitions: {
|
|
329
|
+
newsletterOfferVisible: { type: 'boolean', defaultValue: true },
|
|
330
|
+
userIsLoggedIn: { type: 'boolean', defaultValue: false },
|
|
331
|
+
},
|
|
332
|
+
globalReferences: [],
|
|
333
|
+
globalStateReferences: [],
|
|
334
|
+
}
|
|
335
|
+
const result = generateJSXSyntax(wrappingParent, twoStateParams, options)
|
|
336
|
+
|
|
337
|
+
const expressionChild = result.children[0] as types.JSXExpressionContainer
|
|
338
|
+
const outerLogical = expressionChild.expression as types.LogicalExpression
|
|
339
|
+
const conditionChain = outerLogical.left as types.LogicalExpression
|
|
340
|
+
expect(conditionChain.operator).toBe('||')
|
|
341
|
+
expect((conditionChain.left as types.Identifier).name).toBe('newsletterOfferVisible')
|
|
342
|
+
expect(
|
|
343
|
+
((conditionChain.right as types.UnaryExpression).argument as types.Identifier).name
|
|
344
|
+
).toBe('userIsLoggedIn')
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('mixes a prop-referencing entry into a state-anchored chain with the right prefix', () => {
|
|
348
|
+
const ownerContainer = elementNode('container', {}, [])
|
|
349
|
+
ownerContainer.content.renderingConditions = {
|
|
350
|
+
reference: {
|
|
351
|
+
type: 'dynamic',
|
|
352
|
+
content: { referenceType: 'state', id: 'menuOpen' },
|
|
353
|
+
},
|
|
354
|
+
condition: {
|
|
355
|
+
conditions: [
|
|
356
|
+
{ operation: '===', operand: true },
|
|
357
|
+
{
|
|
358
|
+
operation: '===',
|
|
359
|
+
operand: 'admin',
|
|
360
|
+
reference: {
|
|
361
|
+
type: 'dynamic',
|
|
362
|
+
content: { referenceType: 'prop', id: 'role' },
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
matchingCriteria: 'all',
|
|
367
|
+
},
|
|
368
|
+
} as never
|
|
369
|
+
|
|
370
|
+
const wrappingParent = elementNode('container', {}, [ownerContainer])
|
|
371
|
+
const mixedParams: JSXGenerationParams = {
|
|
372
|
+
...params,
|
|
373
|
+
stateDefinitions: { menuOpen: { type: 'boolean', defaultValue: false } },
|
|
374
|
+
propDefinitions: { role: { type: 'string', defaultValue: 'guest' } },
|
|
375
|
+
globalReferences: [],
|
|
376
|
+
globalStateReferences: [],
|
|
377
|
+
}
|
|
378
|
+
const result = generateJSXSyntax(wrappingParent, mixedParams, options)
|
|
379
|
+
|
|
380
|
+
const expressionChild = result.children[0] as types.JSXExpressionContainer
|
|
381
|
+
const conditionChain = (expressionChild.expression as types.LogicalExpression)
|
|
382
|
+
.left as types.LogicalExpression
|
|
383
|
+
|
|
384
|
+
// `=== true` on the boolean state compiles to the bare identifier.
|
|
385
|
+
const stateComparison = conditionChain.left as types.Identifier
|
|
386
|
+
expect(stateComparison.name).toBe('menuOpen')
|
|
387
|
+
|
|
388
|
+
// The prop entry rides the `props.` prefix from the prefix map.
|
|
389
|
+
const propComparison = conditionChain.right as types.BinaryExpression
|
|
390
|
+
const propMember = propComparison.left as types.MemberExpression
|
|
391
|
+
expect((propMember.object as types.Identifier).name).toBe('props')
|
|
392
|
+
expect((propMember.property as types.Identifier).name).toBe('role')
|
|
393
|
+
expect((propComparison.right as types.StringLiteral).value).toBe('admin')
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it('reads a URL search-param entry off the page address (router.query.<key>)', () => {
|
|
397
|
+
// A listing's categories overview: shown while no category is picked AND no
|
|
398
|
+
// search is typed — the search lives in `?searchKeyword=`, not in a state.
|
|
399
|
+
const ownerContainer = elementNode('container', {}, [])
|
|
400
|
+
ownerContainer.content.renderingConditions = {
|
|
401
|
+
reference: {
|
|
402
|
+
type: 'dynamic',
|
|
403
|
+
content: { referenceType: 'state', id: 'selectedCategory' },
|
|
404
|
+
},
|
|
405
|
+
condition: {
|
|
406
|
+
conditions: [
|
|
407
|
+
{ operation: 'isEmpty' },
|
|
408
|
+
{
|
|
409
|
+
operation: 'isEmpty',
|
|
410
|
+
reference: {
|
|
411
|
+
type: 'dynamic',
|
|
412
|
+
content: {
|
|
413
|
+
referenceType: 'urlSearchParams',
|
|
414
|
+
id: 'searchKeyword',
|
|
415
|
+
refPath: ['searchKeyword'],
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
matchingCriteria: 'all',
|
|
421
|
+
},
|
|
422
|
+
} as never
|
|
423
|
+
|
|
424
|
+
const wrappingParent = elementNode('container', {}, [ownerContainer])
|
|
425
|
+
const listingParams: JSXGenerationParams = {
|
|
426
|
+
...params,
|
|
427
|
+
stateDefinitions: { selectedCategory: { type: 'string', defaultValue: '' } },
|
|
428
|
+
globalReferences: [],
|
|
429
|
+
globalStateReferences: [],
|
|
430
|
+
}
|
|
431
|
+
const result = generateJSXSyntax(wrappingParent, listingParams, options)
|
|
432
|
+
|
|
433
|
+
const expressionChild = result.children[0] as types.JSXExpressionContainer
|
|
434
|
+
const conditionChain = (expressionChild.expression as types.LogicalExpression).left
|
|
435
|
+
expect(generate(conditionChain).code).toBe(
|
|
436
|
+
'(selectedCategory || []).length === 0 && (router.query.searchKeyword || []).length === 0'
|
|
437
|
+
)
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
it('tracks a globalState per-entry reference on params for context wiring', () => {
|
|
441
|
+
const ownerContainer = elementNode('container', {}, [])
|
|
442
|
+
ownerContainer.content.renderingConditions = {
|
|
443
|
+
reference: {
|
|
444
|
+
type: 'dynamic',
|
|
445
|
+
content: { referenceType: 'state', id: 'menuOpen' },
|
|
446
|
+
},
|
|
447
|
+
condition: {
|
|
448
|
+
conditions: [
|
|
449
|
+
{ operation: '===', operand: true },
|
|
450
|
+
{
|
|
451
|
+
operation: '===',
|
|
452
|
+
operand: true,
|
|
453
|
+
reference: {
|
|
454
|
+
type: 'dynamic',
|
|
455
|
+
content: { referenceType: 'globalState', id: 'darkMode' },
|
|
456
|
+
},
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
matchingCriteria: 'all',
|
|
460
|
+
},
|
|
461
|
+
} as never
|
|
462
|
+
|
|
463
|
+
const wrappingParent = elementNode('container', {}, [ownerContainer])
|
|
464
|
+
const globalStateParams: JSXGenerationParams = {
|
|
465
|
+
...params,
|
|
466
|
+
stateDefinitions: { menuOpen: { type: 'boolean', defaultValue: false } },
|
|
467
|
+
globalStateDefinitions: {
|
|
468
|
+
darkMode: { id: 'darkMode', name: 'darkMode', type: 'boolean', defaultValue: false },
|
|
469
|
+
},
|
|
470
|
+
globalReferences: [],
|
|
471
|
+
globalStateReferences: [],
|
|
472
|
+
}
|
|
473
|
+
generateJSXSyntax(wrappingParent, globalStateParams, options)
|
|
474
|
+
|
|
475
|
+
expect(globalStateParams.globalStateReferences).toEqual([
|
|
476
|
+
{ id: 'darkMode', name: 'darkMode' },
|
|
477
|
+
])
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('nests a group entry as a parenthesized sub-chain — (a && b) || c', () => {
|
|
481
|
+
const ownerContainer = elementNode('container', {}, [])
|
|
482
|
+
ownerContainer.content.renderingConditions = {
|
|
483
|
+
reference: {
|
|
484
|
+
type: 'dynamic',
|
|
485
|
+
content: { referenceType: 'state', id: 'a' },
|
|
486
|
+
},
|
|
487
|
+
condition: {
|
|
488
|
+
conditions: [
|
|
489
|
+
{
|
|
490
|
+
conditions: [
|
|
491
|
+
{ operation: '===', operand: 1 },
|
|
492
|
+
{
|
|
493
|
+
operation: '===',
|
|
494
|
+
operand: 3,
|
|
495
|
+
reference: {
|
|
496
|
+
type: 'dynamic',
|
|
497
|
+
content: { referenceType: 'state', id: 'b' },
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
],
|
|
501
|
+
matchingCriteria: 'all',
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
operation: '===',
|
|
505
|
+
operand: 5,
|
|
506
|
+
reference: {
|
|
507
|
+
type: 'dynamic',
|
|
508
|
+
content: { referenceType: 'state', id: 'c' },
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
],
|
|
512
|
+
matchingCriteria: '||',
|
|
513
|
+
},
|
|
514
|
+
} as never
|
|
515
|
+
|
|
516
|
+
const wrappingParent = elementNode('container', {}, [ownerContainer])
|
|
517
|
+
const groupedParams: JSXGenerationParams = {
|
|
518
|
+
...params,
|
|
519
|
+
stateDefinitions: {
|
|
520
|
+
a: { type: 'number', defaultValue: 1 },
|
|
521
|
+
b: { type: 'number', defaultValue: 3 },
|
|
522
|
+
c: { type: 'number', defaultValue: 0 },
|
|
523
|
+
},
|
|
524
|
+
globalReferences: [],
|
|
525
|
+
globalStateReferences: [],
|
|
526
|
+
}
|
|
527
|
+
const result = generateJSXSyntax(wrappingParent, groupedParams, options)
|
|
528
|
+
|
|
529
|
+
const expressionChild = result.children[0] as types.JSXExpressionContainer
|
|
530
|
+
const conditionChain = (expressionChild.expression as types.LogicalExpression)
|
|
531
|
+
.left as types.LogicalExpression
|
|
532
|
+
|
|
533
|
+
// (a === 1 && b === 3) || c === 5 — the group nests as the left branch,
|
|
534
|
+
// which the printer emits parenthesized.
|
|
535
|
+
expect(conditionChain.operator).toBe('||')
|
|
536
|
+
|
|
537
|
+
const groupChain = conditionChain.left as types.LogicalExpression
|
|
538
|
+
expect(groupChain.type).toBe('LogicalExpression')
|
|
539
|
+
expect(groupChain.operator).toBe('&&')
|
|
540
|
+
const aComparison = groupChain.left as types.BinaryExpression
|
|
541
|
+
const bComparison = groupChain.right as types.BinaryExpression
|
|
542
|
+
expect((aComparison.left as types.Identifier).name).toBe('a')
|
|
543
|
+
expect((aComparison.right as types.NumericLiteral).value).toBe(1)
|
|
544
|
+
expect((bComparison.left as types.Identifier).name).toBe('b')
|
|
545
|
+
expect((bComparison.right as types.NumericLiteral).value).toBe(3)
|
|
546
|
+
|
|
547
|
+
const cComparison = conditionChain.right as types.BinaryExpression
|
|
548
|
+
expect((cComparison.left as types.Identifier).name).toBe('c')
|
|
549
|
+
expect((cComparison.right as types.NumericLiteral).value).toBe(5)
|
|
550
|
+
})
|
|
551
|
+
|
|
552
|
+
it('tracks references from leaves nested inside groups', () => {
|
|
553
|
+
const ownerContainer = elementNode('container', {}, [])
|
|
554
|
+
ownerContainer.content.renderingConditions = {
|
|
555
|
+
reference: {
|
|
556
|
+
type: 'dynamic',
|
|
557
|
+
content: { referenceType: 'state', id: 'menuOpen' },
|
|
558
|
+
},
|
|
559
|
+
condition: {
|
|
560
|
+
conditions: [
|
|
561
|
+
{ operation: '===', operand: true },
|
|
562
|
+
{
|
|
563
|
+
conditions: [
|
|
564
|
+
{
|
|
565
|
+
operation: '===',
|
|
566
|
+
operand: true,
|
|
567
|
+
reference: {
|
|
568
|
+
type: 'dynamic',
|
|
569
|
+
content: { referenceType: 'globalState', id: 'darkMode' },
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
],
|
|
573
|
+
},
|
|
574
|
+
],
|
|
575
|
+
matchingCriteria: 'all',
|
|
576
|
+
},
|
|
577
|
+
} as never
|
|
578
|
+
|
|
579
|
+
const wrappingParent = elementNode('container', {}, [ownerContainer])
|
|
580
|
+
const nestedTrackingParams: JSXGenerationParams = {
|
|
581
|
+
...params,
|
|
582
|
+
stateDefinitions: { menuOpen: { type: 'boolean', defaultValue: false } },
|
|
583
|
+
globalStateDefinitions: {
|
|
584
|
+
darkMode: { id: 'darkMode', name: 'darkMode', type: 'boolean', defaultValue: false },
|
|
585
|
+
},
|
|
586
|
+
globalReferences: [],
|
|
587
|
+
globalStateReferences: [],
|
|
588
|
+
}
|
|
589
|
+
generateJSXSyntax(wrappingParent, nestedTrackingParams, options)
|
|
590
|
+
|
|
591
|
+
expect(nestedTrackingParams.globalStateReferences).toEqual([
|
|
592
|
+
{ id: 'darkMode', name: 'darkMode' },
|
|
593
|
+
])
|
|
594
|
+
})
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
describe('navlink href via differentiatorValue', () => {
|
|
598
|
+
// The navlink attribute flow is exercised end-to-end in the resolver tests;
|
|
599
|
+
// here we only confirm that an `expr`-typed attribute which mentions
|
|
600
|
+
// `currentUser` registers a global reference so useGlobalContext() is wired
|
|
601
|
+
// up downstream.
|
|
602
|
+
it('tracks currentUser as a global reference when emitted via an expr attr', () => {
|
|
603
|
+
const linkWithDifferentiatorHref = elementNode('a', {
|
|
604
|
+
href: {
|
|
605
|
+
type: 'expr',
|
|
606
|
+
content: '`/profile/' + '$' + '{' + 'currentUser?.id}' + '`',
|
|
607
|
+
},
|
|
608
|
+
})
|
|
609
|
+
const localParams: JSXGenerationParams = {
|
|
610
|
+
...params,
|
|
611
|
+
globalReferences: [],
|
|
612
|
+
}
|
|
613
|
+
generateJSXSyntax(linkWithDifferentiatorHref, localParams, options)
|
|
614
|
+
expect(localParams.globalReferences).toContain('currentUser')
|
|
615
|
+
})
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
describe('controlled <textarea> must not render children (React SSR contract)', () => {
|
|
619
|
+
const buildParams = (): JSXGenerationParams => ({
|
|
620
|
+
dependencies: {},
|
|
621
|
+
propDefinitions: {},
|
|
622
|
+
stateDefinitions: {},
|
|
623
|
+
globalStateDefinitions: {},
|
|
624
|
+
nodesLookup: {},
|
|
625
|
+
windowImports: {},
|
|
626
|
+
localeReferences: [],
|
|
627
|
+
localeAttributeReferences: [],
|
|
628
|
+
globalReferences: [],
|
|
629
|
+
globalStateReferences: [],
|
|
630
|
+
hoistedConstants: [],
|
|
631
|
+
})
|
|
632
|
+
|
|
633
|
+
it('drops the children of a textarea that has a value attribute', () => {
|
|
634
|
+
// The details/entity binder can add value={{ ctx.field }} to a <textarea>
|
|
635
|
+
// while leaving its original text child in place. React's server renderer
|
|
636
|
+
// throws "If you supply value on a <textarea> you must not supply children",
|
|
637
|
+
// aborting static export — so the children must not be attached.
|
|
638
|
+
const localParams = buildParams()
|
|
639
|
+
const node = elementNode('textarea', { value: dynamicNode('prop', 'description') }, [
|
|
640
|
+
elementNode('span', {}, [dynamicNode('prop', 'description')]),
|
|
641
|
+
])
|
|
642
|
+
const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
|
|
643
|
+
|
|
644
|
+
expect((result.openingElement.name as types.JSXIdentifier).name).toBe('textarea')
|
|
645
|
+
expect(result.children.length).toBe(0)
|
|
646
|
+
// The child <span> is still generated into nodesLookup so the later
|
|
647
|
+
// styled-jsx traversal doesn't fail with "missing from the template chunk".
|
|
648
|
+
expect(Object.keys(localParams.nodesLookup).length).toBeGreaterThanOrEqual(2)
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
it('drops the children of a textarea that has a defaultValue attribute', () => {
|
|
652
|
+
const localParams = buildParams()
|
|
653
|
+
const node = elementNode('textarea', { defaultValue: dynamicNode('prop', 'description') }, [
|
|
654
|
+
elementNode('span', {}, [staticNode('initial')]),
|
|
655
|
+
])
|
|
656
|
+
const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
|
|
657
|
+
expect(result.children.length).toBe(0)
|
|
658
|
+
})
|
|
659
|
+
|
|
660
|
+
it('keeps the children of an uncontrolled textarea (no value/defaultValue)', () => {
|
|
661
|
+
const localParams = buildParams()
|
|
662
|
+
const node = elementNode('textarea', {}, [staticNode('initial content')])
|
|
663
|
+
const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
|
|
664
|
+
expect(result.children.length).toBeGreaterThan(0)
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
it('drops a stray dynamic (element/expression) child of an uncontrolled textarea', () => {
|
|
668
|
+
// Run 97ac2650 "Add Note": a textarea whose value binding arrived as a
|
|
669
|
+
// {{ }} CHILD (no value attr) was demoted to an element child — React
|
|
670
|
+
// stringifies it to the literal "[object Object]". The backstop drops the
|
|
671
|
+
// dynamic child so it can never render "[object Object]". (The primary fix
|
|
672
|
+
// hoists the binding to a value= attr at import time.)
|
|
673
|
+
const localParams = buildParams()
|
|
674
|
+
const node = elementNode('textarea', {}, [
|
|
675
|
+
elementNode('span', {}, [dynamicNode('prop', 'noteBody')]),
|
|
676
|
+
])
|
|
677
|
+
const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
|
|
678
|
+
expect(result.children.length).toBe(0)
|
|
679
|
+
// The dropped child is still generated into nodesLookup (styled-jsx pass).
|
|
680
|
+
expect(Object.keys(localParams.nodesLookup).length).toBeGreaterThanOrEqual(2)
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
it('drops the children of a named textarea controlled by the form-store binding', () => {
|
|
684
|
+
// maybeAddFormStoreFieldBinding injects value={formState['field'] ?? ''}
|
|
685
|
+
// onto named inputs inside a data-store-values-state form AFTER the UIDL
|
|
686
|
+
// attrs are processed — the textarea has NO value in its UIDL attrs but
|
|
687
|
+
// ends up controlled all the same, so its children must also be skipped.
|
|
688
|
+
const localParams = { ...buildParams(), formStoreStateName: 'formValues' }
|
|
689
|
+
const node = elementNode('textarea', { name: staticNode('description') }, [
|
|
690
|
+
staticNode('initial content'),
|
|
691
|
+
])
|
|
692
|
+
const result = generateJSXSyntax(node, localParams, options) as types.JSXElement
|
|
693
|
+
|
|
694
|
+
const hasInjectedValue = result.openingElement.attributes.some(
|
|
695
|
+
(attr) =>
|
|
696
|
+
attr.type === 'JSXAttribute' &&
|
|
697
|
+
attr.name.type === 'JSXIdentifier' &&
|
|
698
|
+
attr.name.name === 'value'
|
|
699
|
+
)
|
|
700
|
+
expect(hasInjectedValue).toBe(true)
|
|
701
|
+
expect(result.children.length).toBe(0)
|
|
702
|
+
})
|
|
703
|
+
})
|
|
704
|
+
|
|
705
|
+
describe('plan v15 Layer 4 — codegen safety net for junk attrs and corrupt values', () => {
|
|
706
|
+
// Suppress the console.warn that fires when junk attrs are dropped so
|
|
707
|
+
// the test runner output stays clean. Reset between tests.
|
|
708
|
+
let warnSpy: jest.SpyInstance | null = null
|
|
709
|
+
beforeEach(() => {
|
|
710
|
+
warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
711
|
+
})
|
|
712
|
+
afterEach(() => {
|
|
713
|
+
warnSpy?.mockRestore()
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
const attributeNames = (el: types.JSXElement): string[] =>
|
|
717
|
+
el.openingElement.attributes
|
|
718
|
+
.filter((a): a is types.JSXAttribute => a.type === 'JSXAttribute')
|
|
719
|
+
.map((a) => (a.name as types.JSXIdentifier).name)
|
|
720
|
+
|
|
721
|
+
const styleAttributeOf = (el: types.JSXElement) =>
|
|
722
|
+
el.openingElement.attributes.find(
|
|
723
|
+
(a): a is types.JSXAttribute =>
|
|
724
|
+
a.type === 'JSXAttribute' && (a.name as types.JSXIdentifier).name === 'style'
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
it('turns a text style attribute into the object React wants (error #62 otherwise)', () => {
|
|
728
|
+
const node = elementNode('div', {
|
|
729
|
+
style: { type: 'static', content: 'color: red; margin-top: 4px; --accent: #fff' },
|
|
730
|
+
} as never)
|
|
731
|
+
const result = generateJSXSyntax(node, params, options) as types.JSXElement
|
|
732
|
+
const styleAttr = styleAttributeOf(result)
|
|
733
|
+
expect(styleAttr?.value?.type).toBe('JSXExpressionContainer')
|
|
734
|
+
const expression = (styleAttr?.value as types.JSXExpressionContainer).expression
|
|
735
|
+
expect(expression.type).toBe('ObjectExpression')
|
|
736
|
+
const keys = (expression as types.ObjectExpression).properties.map((property) => {
|
|
737
|
+
const key = (property as types.ObjectProperty).key
|
|
738
|
+
return key.type === 'Identifier' ? key.name : (key as types.StringLiteral).value
|
|
739
|
+
})
|
|
740
|
+
expect(keys).toEqual(['color', 'marginTop', '--accent'])
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
it('drops a style attribute that is not CSS text instead of emitting a string', () => {
|
|
744
|
+
const node = elementNode('div', {
|
|
745
|
+
style: { type: 'static', content: 'nonsense' },
|
|
746
|
+
class: { type: 'static', content: 'kept' },
|
|
747
|
+
} as never)
|
|
748
|
+
const result = generateJSXSyntax(node, params, options) as types.JSXElement
|
|
749
|
+
expect(styleAttributeOf(result)).toBeUndefined()
|
|
750
|
+
expect(attributeNames(result)).toContain('class')
|
|
751
|
+
})
|
|
752
|
+
|
|
753
|
+
it('drops junk-name attribute true="true"', () => {
|
|
754
|
+
const node = elementNode('div', {
|
|
755
|
+
true: { type: 'static', content: 'true' },
|
|
756
|
+
class: { type: 'static', content: 'kept' },
|
|
757
|
+
} as never)
|
|
758
|
+
const result = generateJSXSyntax(node, params, options) as types.JSXElement
|
|
759
|
+
const names = attributeNames(result)
|
|
760
|
+
expect(names).not.toContain('true')
|
|
761
|
+
expect(names).toContain('class')
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
it('drops junk-name attribute false="false"', () => {
|
|
765
|
+
const node = elementNode('div', {
|
|
766
|
+
false: { type: 'static', content: 'false' },
|
|
767
|
+
class: { type: 'static', content: 'kept' },
|
|
768
|
+
} as never)
|
|
769
|
+
const result = generateJSXSyntax(node, params, options) as types.JSXElement
|
|
770
|
+
expect(attributeNames(result)).not.toContain('false')
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
it('drops numeric-name attributes (e.g. 0="x")', () => {
|
|
774
|
+
const node = elementNode('div', {
|
|
775
|
+
'0': { type: 'static', content: 'x' },
|
|
776
|
+
class: { type: 'static', content: 'kept' },
|
|
777
|
+
} as never)
|
|
778
|
+
const result = generateJSXSyntax(node, params, options) as types.JSXElement
|
|
779
|
+
expect(attributeNames(result)).not.toContain('0')
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
it('drops null and undefined name attrs', () => {
|
|
783
|
+
const node = elementNode('div', {
|
|
784
|
+
null: { type: 'static', content: 'x' },
|
|
785
|
+
undefined: { type: 'static', content: 'y' },
|
|
786
|
+
class: { type: 'static', content: 'kept' },
|
|
787
|
+
} as never)
|
|
788
|
+
const result = generateJSXSyntax(node, params, options) as types.JSXElement
|
|
789
|
+
const names = attributeNames(result)
|
|
790
|
+
expect(names).not.toContain('null')
|
|
791
|
+
expect(names).not.toContain('undefined')
|
|
792
|
+
})
|
|
793
|
+
|
|
794
|
+
it('drops entity-escaped binding markers in static attr values ({)', () => {
|
|
795
|
+
const node = elementNode('tq-if', {
|
|
796
|
+
condition: { type: 'static', content: '{{state.X}}' },
|
|
797
|
+
} as never)
|
|
798
|
+
const result = generateJSXSyntax(node, params, options) as types.JSXElement
|
|
799
|
+
expect(attributeNames(result)).not.toContain('condition')
|
|
800
|
+
})
|
|
801
|
+
|
|
802
|
+
it('drops static attr values with truncated `{{` (no closing `}}`)', () => {
|
|
803
|
+
const node = elementNode('tq-if', {
|
|
804
|
+
condition: { type: 'static', content: '{{state.isMobileMenuOpen' },
|
|
805
|
+
} as never)
|
|
806
|
+
const result = generateJSXSyntax(node, params, options) as types.JSXElement
|
|
807
|
+
expect(attributeNames(result)).not.toContain('condition')
|
|
808
|
+
})
|
|
809
|
+
|
|
810
|
+
it('preserves well-formed static attrs (no false positives)', () => {
|
|
811
|
+
const node = elementNode('div', {
|
|
812
|
+
class: { type: 'static', content: 'foo' },
|
|
813
|
+
'data-config': { type: 'static', content: '{"k":1}' },
|
|
814
|
+
condition: { type: 'static', content: '{{state.X}}' },
|
|
815
|
+
} as never)
|
|
816
|
+
const result = generateJSXSyntax(node, params, options) as types.JSXElement
|
|
817
|
+
const names = attributeNames(result)
|
|
818
|
+
expect(names).toContain('class')
|
|
819
|
+
expect(names).toContain('data-config')
|
|
820
|
+
expect(names).toContain('condition')
|
|
821
|
+
})
|
|
822
|
+
})
|
|
107
823
|
})
|