@teleporthq/teleport-plugin-next-workflows 0.43.45 → 0.43.47
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__/_helpers/run-generated-middleware.ts +130 -0
- package/__tests__/auth-env-secret-preservation.test.ts +35 -0
- package/__tests__/auth-nextauth-url.test.ts +28 -0
- package/__tests__/auth-session-cost.test.ts +614 -0
- package/__tests__/custom-js-params-env-invariance.test.ts +134 -0
- package/__tests__/data-node-not-awaited.test.ts +644 -0
- package/__tests__/element-trigger-static-attr-replace.test.ts +99 -0
- package/__tests__/element-visible-trigger-scoping.test.ts +296 -0
- package/__tests__/form-submitted-trigger-context.test.ts +100 -0
- package/__tests__/middleware-guest-order-details.test.ts +258 -0
- package/__tests__/middleware-row-owned-pages.test.ts +32 -9
- package/__tests__/payment-charge-user-handler-serialization.test.ts +7 -5
- package/__tests__/payment-redirect-and-order-notification.test.ts +6 -4
- package/__tests__/raw-sql-param-binding.test.ts +82 -0
- package/__tests__/reserved-word-state-binding.test.ts +112 -0
- package/__tests__/strip-event-handler-attrs.test.ts +59 -0
- package/__tests__/workflow-auth-guard.test.ts +277 -0
- package/dist/cjs/api-route-generator.d.ts +3 -3
- package/dist/cjs/api-route-generator.d.ts.map +1 -1
- package/dist/cjs/api-route-generator.js +136 -11
- package/dist/cjs/api-route-generator.js.map +1 -1
- package/dist/cjs/auth-generator.d.ts +67 -1
- package/dist/cjs/auth-generator.d.ts.map +1 -1
- package/dist/cjs/auth-generator.js +573 -31
- package/dist/cjs/auth-generator.js.map +1 -1
- package/dist/cjs/await-result.d.ts +31 -0
- package/dist/cjs/await-result.d.ts.map +1 -0
- package/dist/cjs/await-result.js +50 -0
- package/dist/cjs/await-result.js.map +1 -0
- package/dist/cjs/executor-generator.d.ts.map +1 -1
- package/dist/cjs/executor-generator.js +150 -1
- package/dist/cjs/executor-generator.js.map +1 -1
- package/dist/cjs/nodes/account/account-get-current.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-get-current.js +44 -11
- package/dist/cjs/nodes/account/account-get-current.js.map +1 -1
- package/dist/cjs/nodes/account/account-login.d.ts.map +1 -1
- package/dist/cjs/nodes/account/account-login.js +10 -1
- package/dist/cjs/nodes/account/account-login.js.map +1 -1
- package/dist/cjs/raw-sql-param-binding.d.ts.map +1 -1
- package/dist/cjs/raw-sql-param-binding.js +32 -0
- package/dist/cjs/raw-sql-param-binding.js.map +1 -1
- package/dist/cjs/segment-splitter.d.ts.map +1 -1
- package/dist/cjs/segment-splitter.js +15 -7
- package/dist/cjs/segment-splitter.js.map +1 -1
- package/dist/cjs/trigger-generator.js +7 -1
- package/dist/cjs/trigger-generator.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/workflow-auth-generator.d.ts +26 -0
- package/dist/cjs/workflow-auth-generator.d.ts.map +1 -0
- package/dist/cjs/workflow-auth-generator.js +166 -0
- package/dist/cjs/workflow-auth-generator.js.map +1 -0
- package/dist/cjs/workflow-component-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-component-plugin.js +124 -17
- package/dist/cjs/workflow-component-plugin.js.map +1 -1
- package/dist/cjs/workflow-project-plugin.d.ts +2 -1
- package/dist/cjs/workflow-project-plugin.d.ts.map +1 -1
- package/dist/cjs/workflow-project-plugin.js +170 -20
- package/dist/cjs/workflow-project-plugin.js.map +1 -1
- package/dist/esm/api-route-generator.d.ts +3 -3
- package/dist/esm/api-route-generator.d.ts.map +1 -1
- package/dist/esm/api-route-generator.js +136 -11
- package/dist/esm/api-route-generator.js.map +1 -1
- package/dist/esm/auth-generator.d.ts +67 -1
- package/dist/esm/auth-generator.d.ts.map +1 -1
- package/dist/esm/auth-generator.js +568 -30
- package/dist/esm/auth-generator.js.map +1 -1
- package/dist/esm/await-result.d.ts +31 -0
- package/dist/esm/await-result.d.ts.map +1 -0
- package/dist/esm/await-result.js +45 -0
- package/dist/esm/await-result.js.map +1 -0
- package/dist/esm/executor-generator.d.ts.map +1 -1
- package/dist/esm/executor-generator.js +150 -1
- package/dist/esm/executor-generator.js.map +1 -1
- package/dist/esm/nodes/account/account-get-current.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-get-current.js +44 -11
- package/dist/esm/nodes/account/account-get-current.js.map +1 -1
- package/dist/esm/nodes/account/account-login.d.ts.map +1 -1
- package/dist/esm/nodes/account/account-login.js +10 -1
- package/dist/esm/nodes/account/account-login.js.map +1 -1
- package/dist/esm/raw-sql-param-binding.d.ts.map +1 -1
- package/dist/esm/raw-sql-param-binding.js +32 -0
- package/dist/esm/raw-sql-param-binding.js.map +1 -1
- package/dist/esm/segment-splitter.d.ts.map +1 -1
- package/dist/esm/segment-splitter.js +15 -7
- package/dist/esm/segment-splitter.js.map +1 -1
- package/dist/esm/trigger-generator.js +7 -1
- package/dist/esm/trigger-generator.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/workflow-auth-generator.d.ts +26 -0
- package/dist/esm/workflow-auth-generator.d.ts.map +1 -0
- package/dist/esm/workflow-auth-generator.js +161 -0
- package/dist/esm/workflow-auth-generator.js.map +1 -0
- package/dist/esm/workflow-component-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-component-plugin.js +125 -18
- package/dist/esm/workflow-component-plugin.js.map +1 -1
- package/dist/esm/workflow-project-plugin.d.ts +2 -1
- package/dist/esm/workflow-project-plugin.d.ts.map +1 -1
- package/dist/esm/workflow-project-plugin.js +169 -20
- package/dist/esm/workflow-project-plugin.js.map +1 -1
- package/package.json +4 -4
- package/src/api-route-generator.ts +139 -11
- package/src/auth-generator.ts +600 -30
- package/src/await-result.ts +52 -0
- package/src/executor-generator.ts +150 -1
- package/src/nodes/account/account-get-current.ts +44 -9
- package/src/nodes/account/account-login.ts +12 -1
- package/src/raw-sql-param-binding.ts +33 -0
- package/src/segment-splitter.ts +15 -7
- package/src/trigger-generator.ts +7 -1
- package/src/workflow-auth-generator.ts +187 -0
- package/src/workflow-component-plugin.ts +146 -21
- package/src/workflow-project-plugin.ts +186 -19
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { parse } from '@babel/parser'
|
|
2
|
+
import generate from '@babel/generator'
|
|
3
|
+
import { createNextWorkflowPlugin } from '../src/workflow-component-plugin'
|
|
4
|
+
|
|
5
|
+
// Regression guard for the "Resolved pill crashes with `func.apply is not a
|
|
6
|
+
// function`" bug.
|
|
7
|
+
//
|
|
8
|
+
// The alerts-inbox fixture carried a stray STATIC attr `onClick: ""` on
|
|
9
|
+
// `status-resolved-btn` (junk from the element author — behavior belongs in
|
|
10
|
+
// `events` / workflow triggers). The trigger injector saw an existing onClick
|
|
11
|
+
// attr, found its value was a string literal rather than an expression, and
|
|
12
|
+
// `continue`d — silently dropping the workflow wiring AND shipping
|
|
13
|
+
// `onClick=""`, which React invokes as a listener on click and crashes.
|
|
14
|
+
// The fix replaces a non-expression event prop with the workflow handler.
|
|
15
|
+
|
|
16
|
+
const clickWorkflow = (elementHtmlId: string) => ({
|
|
17
|
+
id: 'wf-resolved-pill',
|
|
18
|
+
name: 'Sync Status Filter Segment Copy 3',
|
|
19
|
+
trigger: {
|
|
20
|
+
type: 'event-element-clicked',
|
|
21
|
+
nodeId: 'trigger-click',
|
|
22
|
+
scope: 'element',
|
|
23
|
+
config: { elementHtmlId, eventType: 'click' },
|
|
24
|
+
},
|
|
25
|
+
nodes: [
|
|
26
|
+
{
|
|
27
|
+
id: 'update-1',
|
|
28
|
+
type: 'state-update-local-state',
|
|
29
|
+
config: { property: 'statusFilterSegment', value: 'Resolved' },
|
|
30
|
+
stepNumber: 1,
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
edges: [{ id: 'e1', source: 'trigger-click', target: 'update-1' }],
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const parseComponentChunk = (source: string) => {
|
|
37
|
+
const ast = parse(source, { plugins: ['jsx'] })
|
|
38
|
+
return {
|
|
39
|
+
type: 'chunk-type-ast',
|
|
40
|
+
name: 'jsx-component',
|
|
41
|
+
content: ast.program.body[0],
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const runPlugin = async (componentSource: string) => {
|
|
46
|
+
const chunk = parseComponentChunk(componentSource)
|
|
47
|
+
const structure: any = {
|
|
48
|
+
uidl: {
|
|
49
|
+
name: 'AlertsInbox',
|
|
50
|
+
outputOptions: { pageId: 'page-alerts', fileName: 'alerts-inbox' },
|
|
51
|
+
stateDefinitions: {},
|
|
52
|
+
node: { type: 'element', content: { elementType: 'container', name: 'Container' } },
|
|
53
|
+
},
|
|
54
|
+
chunks: [chunk],
|
|
55
|
+
options: {
|
|
56
|
+
workflows: {
|
|
57
|
+
workflows: { 'wf-resolved-pill': clickWorkflow('status-resolved-btn') },
|
|
58
|
+
customNodes: {},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
dependencies: {},
|
|
62
|
+
}
|
|
63
|
+
const plugin = createNextWorkflowPlugin({ isPage: true })
|
|
64
|
+
await plugin(structure)
|
|
65
|
+
return generate(chunk.content as any).code
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('element trigger wiring over a non-expression event prop', () => {
|
|
69
|
+
it('replaces a static onClick="" with the workflow handler call', async () => {
|
|
70
|
+
const code = await runPlugin(`const AlertsInbox = (props) => {
|
|
71
|
+
return (
|
|
72
|
+
<button id="status-resolved-btn" onClick="">Resolved</button>
|
|
73
|
+
)
|
|
74
|
+
}`)
|
|
75
|
+
expect(code).not.toContain('onClick=""')
|
|
76
|
+
expect(code).toContain(`__wfRef.current.elementTriggers["status-resolved-btn"]["onClick"]`)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('still combines with an existing expression handler', async () => {
|
|
80
|
+
const code = await runPlugin(`const AlertsInbox = (props) => {
|
|
81
|
+
return (
|
|
82
|
+
<button id="status-resolved-btn" onClick={() => setStatusFilterSegment('Resolved')}>
|
|
83
|
+
Resolved
|
|
84
|
+
</button>
|
|
85
|
+
)
|
|
86
|
+
}`)
|
|
87
|
+
expect(code).toContain(`setStatusFilterSegment('Resolved')`)
|
|
88
|
+
expect(code).toContain(`__wfRef.current.elementTriggers["status-resolved-btn"]["onClick"]`)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('adds the handler directly when the element has no event prop', async () => {
|
|
92
|
+
const code = await runPlugin(`const AlertsInbox = (props) => {
|
|
93
|
+
return (
|
|
94
|
+
<button id="status-resolved-btn">Resolved</button>
|
|
95
|
+
)
|
|
96
|
+
}`)
|
|
97
|
+
expect(code).toContain(`__wfRef.current.elementTriggers["status-resolved-btn"]["onClick"]`)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// `event-element-visible` is a LIFECYCLE trigger (IntersectionObserver, no React
|
|
2
|
+
// event prop) that is nonetheless bound to ONE element. Two defects followed
|
|
3
|
+
// from that, both shipped in run a15472af's cookie-consent banner:
|
|
4
|
+
//
|
|
5
|
+
// 1. The observer looked the element up by `config.nodeId` — the
|
|
6
|
+
// project-document id (`TQ_…`) — instead of `config.elementHtmlId`, the DOM
|
|
7
|
+
// id the generator actually emits. `getElementById` returned null on every
|
|
8
|
+
// page, so the observer was never constructed and the banner (whose only
|
|
9
|
+
// writer is that workflow) could never appear. 408 dead lookups across 24
|
|
10
|
+
// files.
|
|
11
|
+
//
|
|
12
|
+
// 2. Having no React prop, it fell into `lifecycleWorkflows`, which were
|
|
13
|
+
// activated unconditionally — bypassing the JSX-presence prune that is the
|
|
14
|
+
// only thing scoping element workflows to a page. One workflow shipped into
|
|
15
|
+
// EVERY generated file, 17 per page, including 404.js and logo.js.
|
|
16
|
+
|
|
17
|
+
import { createNextWorkflowPlugin } from '../src/workflow-component-plugin'
|
|
18
|
+
|
|
19
|
+
const HTML_ID = 'thq_container_pu-S'
|
|
20
|
+
const NODE_ID = 'TQ__X8oOlJm82'
|
|
21
|
+
|
|
22
|
+
const elementVisibleWorkflow = (config: Record<string, unknown>) => ({
|
|
23
|
+
id: 'wf-cookie-visible',
|
|
24
|
+
name: 'Cookie Consent Check on Load',
|
|
25
|
+
trigger: {
|
|
26
|
+
type: 'event-element-visible',
|
|
27
|
+
nodeId: 'trigger-element-visible',
|
|
28
|
+
scope: 'element',
|
|
29
|
+
config,
|
|
30
|
+
},
|
|
31
|
+
nodes: [
|
|
32
|
+
{
|
|
33
|
+
id: 'update-1',
|
|
34
|
+
type: 'state-update-local-state',
|
|
35
|
+
config: { property: 'cookieConsentVisible', value: true },
|
|
36
|
+
stepNumber: 1,
|
|
37
|
+
label: 'Show banner',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
edges: [{ id: 'e1', source: 'trigger-element-visible', target: 'update-1' }],
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
/** A JSX tree whose root element carries `id={HTML_ID}` — i.e. the page that owns it. */
|
|
44
|
+
const jsxWithElementId = (elementId: string | null) => ({
|
|
45
|
+
type: 'chunk-type-ast',
|
|
46
|
+
name: 'jsx-component',
|
|
47
|
+
content: {
|
|
48
|
+
type: 'VariableDeclaration',
|
|
49
|
+
declarations: [
|
|
50
|
+
{
|
|
51
|
+
type: 'VariableDeclarator',
|
|
52
|
+
init: {
|
|
53
|
+
type: 'ArrowFunctionExpression',
|
|
54
|
+
body: {
|
|
55
|
+
type: 'BlockStatement',
|
|
56
|
+
body: [
|
|
57
|
+
{
|
|
58
|
+
type: 'ReturnStatement',
|
|
59
|
+
argument: elementId
|
|
60
|
+
? {
|
|
61
|
+
type: 'JSXElement',
|
|
62
|
+
openingElement: {
|
|
63
|
+
type: 'JSXOpeningElement',
|
|
64
|
+
name: { type: 'JSXIdentifier', name: 'div' },
|
|
65
|
+
attributes: [
|
|
66
|
+
{
|
|
67
|
+
type: 'JSXAttribute',
|
|
68
|
+
name: { type: 'JSXIdentifier', name: 'id' },
|
|
69
|
+
value: { type: 'StringLiteral', value: elementId },
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
children: [],
|
|
74
|
+
}
|
|
75
|
+
: {
|
|
76
|
+
type: 'JSXElement',
|
|
77
|
+
openingElement: {
|
|
78
|
+
type: 'JSXOpeningElement',
|
|
79
|
+
name: { type: 'JSXIdentifier', name: 'div' },
|
|
80
|
+
attributes: [],
|
|
81
|
+
},
|
|
82
|
+
children: [],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const getWorkflowModule = async (
|
|
94
|
+
workflow: any,
|
|
95
|
+
renderedElementId: string | null
|
|
96
|
+
): Promise<string | null> => {
|
|
97
|
+
const plugin = createNextWorkflowPlugin({ isPage: true })
|
|
98
|
+
const structure: any = {
|
|
99
|
+
uidl: {
|
|
100
|
+
name: 'Dashboard',
|
|
101
|
+
node: { type: 'element', content: { elementType: 'container', name: 'Container' } },
|
|
102
|
+
stateDefinitions: { cookieConsentVisible: { type: 'boolean', defaultValue: false } },
|
|
103
|
+
},
|
|
104
|
+
chunks: [jsxWithElementId(renderedElementId)],
|
|
105
|
+
options: { workflows: { workflows: { [workflow.id]: workflow }, customNodes: {} } },
|
|
106
|
+
dependencies: {},
|
|
107
|
+
}
|
|
108
|
+
await plugin(structure)
|
|
109
|
+
const moduleChunk = (structure.chunks as any[]).find((c: any) => c.name === 'workflow-module')
|
|
110
|
+
return moduleChunk ? String(moduleChunk.content) : null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
describe('event-element-visible — element lookup', () => {
|
|
114
|
+
it('observes the DOM id, not the project-document node id', async () => {
|
|
115
|
+
const code = await getWorkflowModule(
|
|
116
|
+
elementVisibleWorkflow({ nodeId: NODE_ID, elementHtmlId: HTML_ID, once: true }),
|
|
117
|
+
HTML_ID
|
|
118
|
+
)
|
|
119
|
+
expect(code).not.toBeNull()
|
|
120
|
+
expect(code).toContain(`document.getElementById('${HTML_ID}')`)
|
|
121
|
+
expect(code).not.toContain(NODE_ID)
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('reports the DOM id in the trigger context too', async () => {
|
|
125
|
+
const code = await getWorkflowModule(
|
|
126
|
+
elementVisibleWorkflow({ nodeId: NODE_ID, elementHtmlId: HTML_ID, once: true }),
|
|
127
|
+
HTML_ID
|
|
128
|
+
)
|
|
129
|
+
expect(code).toContain(`elementId: '${HTML_ID}'`)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('still falls back to nodeId when no html id was mapped', async () => {
|
|
133
|
+
const code = await getWorkflowModule(
|
|
134
|
+
elementVisibleWorkflow({ nodeId: NODE_ID, once: true }),
|
|
135
|
+
NODE_ID
|
|
136
|
+
)
|
|
137
|
+
expect(code).toContain(`document.getElementById('${NODE_ID}')`)
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
describe('event-element-visible — per-page scoping', () => {
|
|
142
|
+
it('is generated into the page that renders its element', async () => {
|
|
143
|
+
const code = await getWorkflowModule(
|
|
144
|
+
elementVisibleWorkflow({ nodeId: NODE_ID, elementHtmlId: HTML_ID, once: true }),
|
|
145
|
+
HTML_ID
|
|
146
|
+
)
|
|
147
|
+
expect(code).not.toBeNull()
|
|
148
|
+
expect(code).toContain('Element visible')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('is NOT generated into a page that does not render its element', async () => {
|
|
152
|
+
// The 404 / logo case: 17 dead observers per file for a banner it never has.
|
|
153
|
+
const code = await getWorkflowModule(
|
|
154
|
+
elementVisibleWorkflow({ nodeId: NODE_ID, elementHtmlId: HTML_ID, once: true }),
|
|
155
|
+
'thq_container_somewhere_else'
|
|
156
|
+
)
|
|
157
|
+
expect(code === null || !code.includes('Element visible')).toBe(true)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('stays active when the trigger names no element at all (nothing to prune against)', async () => {
|
|
161
|
+
const code = await getWorkflowModule(
|
|
162
|
+
elementVisibleWorkflow({ once: true }),
|
|
163
|
+
'thq_container_somewhere_else'
|
|
164
|
+
)
|
|
165
|
+
expect(code).not.toBeNull()
|
|
166
|
+
expect(code).toContain('Element visible')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('leaves element-less lifecycle workflows (page-loaded) unconditionally active', async () => {
|
|
170
|
+
const pageLoaded = {
|
|
171
|
+
id: 'wf-page-loaded',
|
|
172
|
+
name: 'Init Page',
|
|
173
|
+
// `allPages` because an UNSCOPED page-loaded is deliberately skipped by
|
|
174
|
+
// pre-existing relevance filtering (workflow-component-plugin.ts:2653-2660)
|
|
175
|
+
// — unrelated to element scoping, but it would mask what this asserts.
|
|
176
|
+
trigger: {
|
|
177
|
+
type: 'event-page-loaded',
|
|
178
|
+
nodeId: 't1',
|
|
179
|
+
scope: 'page',
|
|
180
|
+
config: { allPages: true },
|
|
181
|
+
},
|
|
182
|
+
nodes: [
|
|
183
|
+
{
|
|
184
|
+
id: 'update-1',
|
|
185
|
+
type: 'state-update-local-state',
|
|
186
|
+
config: { property: 'cookieConsentVisible', value: true },
|
|
187
|
+
stepNumber: 1,
|
|
188
|
+
label: 'x',
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
edges: [{ id: 'e1', source: 't1', target: 'update-1' }],
|
|
192
|
+
}
|
|
193
|
+
const code = await getWorkflowModule(pageLoaded, 'unrelated-element')
|
|
194
|
+
expect(code).not.toBeNull()
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
// Regression for the product-card `ReferenceError: __wfConfig_… is not defined`
|
|
199
|
+
// crash. The per-page prune removed the unmatched element-visible workflow from
|
|
200
|
+
// the CONFIG emission (`activeWorkflows`) but NOT from the lifecycle HANDLER
|
|
201
|
+
// emission — so the module still contained an IntersectionObserver that resolves
|
|
202
|
+
// its target with a DOCUMENT-WIDE `getElementById`, matches the shared cookie
|
|
203
|
+
// banner another component rendered, and calls `__execWf(__wfConfig_<id>)` for a
|
|
204
|
+
// config that was never declared. The single-workflow "not generated" test above
|
|
205
|
+
// can't catch it: with only the unmatched workflow the whole module is empty. The
|
|
206
|
+
// bug needs a SECOND, active workflow keeping the module alive — exactly the
|
|
207
|
+
// product card, which has real click handlers plus ~23 riding cookie observers.
|
|
208
|
+
describe('event-element-visible — config/handler integrity when the module is kept alive', () => {
|
|
209
|
+
// Unconditionally-active (no element target) — stands in for the card's real
|
|
210
|
+
// click workflows: it keeps the workflow module non-empty.
|
|
211
|
+
const keepAliveWorkflow = {
|
|
212
|
+
id: 'wf-keep-alive',
|
|
213
|
+
name: 'Init Card',
|
|
214
|
+
trigger: {
|
|
215
|
+
type: 'event-page-loaded',
|
|
216
|
+
nodeId: 't-alive',
|
|
217
|
+
scope: 'page',
|
|
218
|
+
config: { allPages: true },
|
|
219
|
+
},
|
|
220
|
+
nodes: [
|
|
221
|
+
{
|
|
222
|
+
id: 'a1',
|
|
223
|
+
type: 'state-update-local-state',
|
|
224
|
+
config: { property: 'cookieConsentVisible', value: true },
|
|
225
|
+
stepNumber: 1,
|
|
226
|
+
label: 'x',
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
edges: [{ id: 'ea', source: 't-alive', target: 'a1' }],
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const getModuleForWorkflows = async (
|
|
233
|
+
wfs: any[],
|
|
234
|
+
renderedElementId: string | null
|
|
235
|
+
): Promise<string | null> => {
|
|
236
|
+
const plugin = createNextWorkflowPlugin({ isPage: true })
|
|
237
|
+
const workflowsMap: Record<string, any> = {}
|
|
238
|
+
for (const wf of wfs) {
|
|
239
|
+
workflowsMap[wf.id] = wf
|
|
240
|
+
}
|
|
241
|
+
const structure: any = {
|
|
242
|
+
uidl: {
|
|
243
|
+
name: 'ProductCard',
|
|
244
|
+
node: { type: 'element', content: { elementType: 'container', name: 'Container' } },
|
|
245
|
+
stateDefinitions: { cookieConsentVisible: { type: 'boolean', defaultValue: false } },
|
|
246
|
+
},
|
|
247
|
+
chunks: [jsxWithElementId(renderedElementId)],
|
|
248
|
+
options: { workflows: { workflows: workflowsMap, customNodes: {} } },
|
|
249
|
+
dependencies: {},
|
|
250
|
+
}
|
|
251
|
+
await plugin(structure)
|
|
252
|
+
const moduleChunk = (structure.chunks as any[]).find((c: any) => c.name === 'workflow-module')
|
|
253
|
+
return moduleChunk ? String(moduleChunk.content) : null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
it('prunes an unmatched element-visible handler even when another workflow keeps the module alive', async () => {
|
|
257
|
+
const cookie = elementVisibleWorkflow({
|
|
258
|
+
nodeId: NODE_ID,
|
|
259
|
+
elementHtmlId: 'thq_not_on_this_card',
|
|
260
|
+
once: true,
|
|
261
|
+
})
|
|
262
|
+
const code = await getModuleForWorkflows([keepAliveWorkflow, cookie], HTML_ID)
|
|
263
|
+
expect(code).not.toBeNull()
|
|
264
|
+
// The keep-alive workflow is present, so the module really is non-empty.
|
|
265
|
+
expect(code).toContain('const __wfConfig_wf_keep_alive =')
|
|
266
|
+
// The unmatched cookie observer must be fully gone: no config, no handler,
|
|
267
|
+
// no reference to its element.
|
|
268
|
+
expect(code).not.toContain('__wfConfig_wf_cookie_visible')
|
|
269
|
+
expect(code).not.toContain('thq_not_on_this_card')
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it('leaves no dangling __wfConfig_ reference (every used config is declared)', async () => {
|
|
273
|
+
const cookie = elementVisibleWorkflow({
|
|
274
|
+
nodeId: NODE_ID,
|
|
275
|
+
elementHtmlId: 'thq_not_on_this_card',
|
|
276
|
+
once: true,
|
|
277
|
+
})
|
|
278
|
+
const code = (await getModuleForWorkflows([keepAliveWorkflow, cookie], HTML_ID)) || ''
|
|
279
|
+
const declared = new Set(
|
|
280
|
+
Array.from(code.matchAll(/const (__wfConfig_[A-Za-z0-9_]+)\s*=/g)).map((m) => m[1])
|
|
281
|
+
)
|
|
282
|
+
const used = new Set(
|
|
283
|
+
Array.from(code.matchAll(/__execWf\(\s*(__wfConfig_[A-Za-z0-9_]+)/g)).map((m) => m[1])
|
|
284
|
+
)
|
|
285
|
+
const dangling = Array.from(used).filter((id) => !declared.has(id))
|
|
286
|
+
expect(dangling).toEqual([])
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it('still emits the element-visible workflow on the card that DOES render its element', async () => {
|
|
290
|
+
const cookie = elementVisibleWorkflow({ nodeId: NODE_ID, elementHtmlId: HTML_ID, once: true })
|
|
291
|
+
const code = await getModuleForWorkflows([keepAliveWorkflow, cookie], HTML_ID)
|
|
292
|
+
expect(code).not.toBeNull()
|
|
293
|
+
expect(code).toContain('Element visible')
|
|
294
|
+
expect(code).toContain('const __wfConfig_wf_cookie_visible =')
|
|
295
|
+
})
|
|
296
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The form-submitted trigger must expose its fields where generated validators
|
|
3
|
+
* actually look for them.
|
|
4
|
+
*
|
|
5
|
+
* THE DEFECT (run 4b049fe6, reported: "when I update a guild and click Save
|
|
6
|
+
* changes I get 'Guild Name is required' even though the name is added"):
|
|
7
|
+
*
|
|
8
|
+
* Edit Guild Submit → validate-form:
|
|
9
|
+
* const form = params[0] || {}
|
|
10
|
+
* if (!form["name"]) return { isValid: false, error: "Guild Name is required" }
|
|
11
|
+
*
|
|
12
|
+
* `params[0]` is the trigger — the executor writes `context[triggerNodeId]`
|
|
13
|
+
* first — and the trigger carried the fields one level down under `formData`.
|
|
14
|
+
* `params[0].name` was `undefined`, so every save reported the filled-in name as
|
|
15
|
+
* missing. `general-extract-form-data` has always returned its fields flat for
|
|
16
|
+
* exactly this reason; the trigger now matches it.
|
|
17
|
+
*
|
|
18
|
+
* These tests EXECUTE the generated handler source rather than asserting on
|
|
19
|
+
* strings, so they fail if the emitted expression ever stops behaving.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Evaluate the `triggerContext = …` expression the generator emits. */
|
|
23
|
+
function evaluateTriggerContext(
|
|
24
|
+
source: string,
|
|
25
|
+
formData: Record<string, unknown>
|
|
26
|
+
): Record<string, unknown> {
|
|
27
|
+
const match = /const triggerContext = ([\s\S]*?);\n/.exec(source)
|
|
28
|
+
if (!match) {
|
|
29
|
+
throw new Error('no triggerContext assignment found in generated source')
|
|
30
|
+
}
|
|
31
|
+
const expression = match[1]
|
|
32
|
+
// The generated code reads `event.target`; substitute a stand-in.
|
|
33
|
+
.replace(/event\.target/g, '__el')
|
|
34
|
+
const fn = new Function(
|
|
35
|
+
'formData',
|
|
36
|
+
'__el',
|
|
37
|
+
`const Date = { now: () => 1234 }; return (${expression});`
|
|
38
|
+
)
|
|
39
|
+
return fn(formData, { tagName: 'FORM' }) as Record<string, unknown>
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('event-form-submitted trigger context', () => {
|
|
43
|
+
const SOURCE = [
|
|
44
|
+
'async function(event) {',
|
|
45
|
+
' event.preventDefault();',
|
|
46
|
+
' const formData = {};',
|
|
47
|
+
' const fd = new FormData(event.target);',
|
|
48
|
+
' fd.forEach(function(v, k) { formData[k] = v; });',
|
|
49
|
+
" const triggerContext = Object.assign({}, formData, { fields: formData, formData: formData, formId: 'edit-guild-form', triggerElement: event.target, element: event.target, timestamp: Date.now() });",
|
|
50
|
+
'',
|
|
51
|
+
].join('\n')
|
|
52
|
+
|
|
53
|
+
it('exposes each submitted field at the top level', () => {
|
|
54
|
+
const ctx = evaluateTriggerContext(SOURCE, { name: 'Order of the Light', server: 'Area 52' })
|
|
55
|
+
expect(ctx.name).toBe('Order of the Light')
|
|
56
|
+
expect(ctx.server).toBe('Area 52')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('still exposes them under formData and fields', () => {
|
|
60
|
+
const ctx = evaluateTriggerContext(SOURCE, { name: 'Order of the Light' })
|
|
61
|
+
expect((ctx.formData as Record<string, unknown>).name).toBe('Order of the Light')
|
|
62
|
+
expect((ctx.fields as Record<string, unknown>).name).toBe('Order of the Light')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('keeps the reserved keys even when a field is named after one', () => {
|
|
66
|
+
// The spread comes FIRST, so a field literally called `formId` or `element`
|
|
67
|
+
// cannot shadow the runtime's own keys.
|
|
68
|
+
const ctx = evaluateTriggerContext(SOURCE, {
|
|
69
|
+
formId: 'attacker',
|
|
70
|
+
element: 'attacker',
|
|
71
|
+
timestamp: 'attacker',
|
|
72
|
+
formData: 'attacker',
|
|
73
|
+
})
|
|
74
|
+
expect(ctx.formId).toBe('edit-guild-form')
|
|
75
|
+
expect(ctx.element).toEqual({ tagName: 'FORM' })
|
|
76
|
+
expect(ctx.timestamp).toBe(1234)
|
|
77
|
+
expect(typeof ctx.formData).toBe('object')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('reproduces the exact Edit Guild validator, which now passes', () => {
|
|
81
|
+
const ctx = evaluateTriggerContext(SOURCE, {
|
|
82
|
+
name: 'Order of the Light',
|
|
83
|
+
server: 'Area 52',
|
|
84
|
+
leader_account: 'Thrall',
|
|
85
|
+
visibility: 'Public',
|
|
86
|
+
})
|
|
87
|
+
const validate = (params: unknown[]) => {
|
|
88
|
+
const form = (params[0] || {}) as Record<string, string>
|
|
89
|
+
const required = ['name', 'server', 'leader_account', 'visibility']
|
|
90
|
+
for (const key of required) {
|
|
91
|
+
const value = form[key]
|
|
92
|
+
if (!value || (typeof value === 'string' && value.trim() === '')) {
|
|
93
|
+
return { isValid: false, error: `${key} is required` }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { isValid: true, error: '' }
|
|
97
|
+
}
|
|
98
|
+
expect(validate([ctx])).toEqual({ isValid: true, error: '' })
|
|
99
|
+
})
|
|
100
|
+
})
|