@teleporthq/teleport-project-generator-html 0.43.70 → 0.43.72
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/state-site.ts +210 -0
- package/__tests__/conditionals/index.ts +208 -0
- package/__tests__/end2end/index.ts +74 -1
- package/__tests__/global-css/index.ts +47 -0
- package/__tests__/motion/index.ts +375 -0
- package/__tests__/page-transition/index.ts +356 -0
- package/__tests__/state-runtime/index.ts +174 -0
- package/__tests__/state-runtime/runtime.ts +225 -0
- package/dist/cjs/global-css.d.ts +22 -0
- package/dist/cjs/global-css.d.ts.map +1 -0
- package/dist/cjs/global-css.js +61 -0
- package/dist/cjs/global-css.js.map +1 -0
- package/dist/cjs/index.d.ts +6 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +16 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/motion/contract.d.ts +31 -0
- package/dist/cjs/motion/contract.d.ts.map +1 -0
- package/dist/cjs/motion/contract.js +73 -0
- package/dist/cjs/motion/contract.js.map +1 -0
- package/dist/cjs/motion/runtime-css.d.ts +21 -0
- package/dist/cjs/motion/runtime-css.d.ts.map +1 -0
- package/dist/cjs/motion/runtime-css.js +95 -0
- package/dist/cjs/motion/runtime-css.js.map +1 -0
- package/dist/cjs/motion/runtime-script.d.ts +7 -0
- package/dist/cjs/motion/runtime-script.d.ts.map +1 -0
- package/dist/cjs/motion/runtime-script.js +52 -0
- package/dist/cjs/motion/runtime-script.js.map +1 -0
- package/dist/cjs/motion/uidl-transform.d.ts +22 -0
- package/dist/cjs/motion/uidl-transform.d.ts.map +1 -0
- package/dist/cjs/motion/uidl-transform.js +148 -0
- package/dist/cjs/motion/uidl-transform.js.map +1 -0
- package/dist/cjs/plugin-motion-runtime.d.ts +8 -0
- package/dist/cjs/plugin-motion-runtime.d.ts.map +1 -0
- package/dist/cjs/plugin-motion-runtime.js +178 -0
- package/dist/cjs/plugin-motion-runtime.js.map +1 -0
- package/dist/cjs/plugin-page-transition.d.ts +8 -0
- package/dist/cjs/plugin-page-transition.d.ts.map +1 -0
- package/dist/cjs/plugin-page-transition.js +187 -0
- package/dist/cjs/plugin-page-transition.js.map +1 -0
- package/dist/cjs/plugin-scroll-rail.d.ts +20 -0
- package/dist/cjs/plugin-scroll-rail.d.ts.map +1 -0
- package/dist/cjs/plugin-scroll-rail.js +107 -0
- package/dist/cjs/plugin-scroll-rail.js.map +1 -0
- package/dist/cjs/plugin-snap-into-view.d.ts +18 -0
- package/dist/cjs/plugin-snap-into-view.d.ts.map +1 -0
- package/dist/cjs/plugin-snap-into-view.js +99 -0
- package/dist/cjs/plugin-snap-into-view.js.map +1 -0
- package/dist/cjs/plugin-state-runtime.d.ts +7 -0
- package/dist/cjs/plugin-state-runtime.d.ts.map +1 -0
- package/dist/cjs/plugin-state-runtime.js +128 -0
- package/dist/cjs/plugin-state-runtime.js.map +1 -0
- package/dist/cjs/state/runtime-script.d.ts +20 -0
- package/dist/cjs/state/runtime-script.d.ts.map +1 -0
- package/dist/cjs/state/runtime-script.js +31 -0
- package/dist/cjs/state/runtime-script.js.map +1 -0
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/global-css.d.ts +22 -0
- package/dist/esm/global-css.d.ts.map +1 -0
- package/dist/esm/global-css.js +55 -0
- package/dist/esm/global-css.js.map +1 -0
- package/dist/esm/index.d.ts +6 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +6 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/motion/contract.d.ts +31 -0
- package/dist/esm/motion/contract.d.ts.map +1 -0
- package/dist/esm/motion/contract.js +69 -0
- package/dist/esm/motion/contract.js.map +1 -0
- package/dist/esm/motion/runtime-css.d.ts +21 -0
- package/dist/esm/motion/runtime-css.d.ts.map +1 -0
- package/dist/esm/motion/runtime-css.js +89 -0
- package/dist/esm/motion/runtime-css.js.map +1 -0
- package/dist/esm/motion/runtime-script.d.ts +7 -0
- package/dist/esm/motion/runtime-script.d.ts.map +1 -0
- package/dist/esm/motion/runtime-script.js +48 -0
- package/dist/esm/motion/runtime-script.js.map +1 -0
- package/dist/esm/motion/uidl-transform.d.ts +22 -0
- package/dist/esm/motion/uidl-transform.d.ts.map +1 -0
- package/dist/esm/motion/uidl-transform.js +144 -0
- package/dist/esm/motion/uidl-transform.js.map +1 -0
- package/dist/esm/plugin-motion-runtime.d.ts +8 -0
- package/dist/esm/plugin-motion-runtime.d.ts.map +1 -0
- package/dist/esm/plugin-motion-runtime.js +175 -0
- package/dist/esm/plugin-motion-runtime.js.map +1 -0
- package/dist/esm/plugin-page-transition.d.ts +8 -0
- package/dist/esm/plugin-page-transition.d.ts.map +1 -0
- package/dist/esm/plugin-page-transition.js +184 -0
- package/dist/esm/plugin-page-transition.js.map +1 -0
- package/dist/esm/plugin-scroll-rail.d.ts +20 -0
- package/dist/esm/plugin-scroll-rail.d.ts.map +1 -0
- package/dist/esm/plugin-scroll-rail.js +104 -0
- package/dist/esm/plugin-scroll-rail.js.map +1 -0
- package/dist/esm/plugin-snap-into-view.d.ts +18 -0
- package/dist/esm/plugin-snap-into-view.d.ts.map +1 -0
- package/dist/esm/plugin-snap-into-view.js +95 -0
- package/dist/esm/plugin-snap-into-view.js.map +1 -0
- package/dist/esm/plugin-state-runtime.d.ts +7 -0
- package/dist/esm/plugin-state-runtime.d.ts.map +1 -0
- package/dist/esm/plugin-state-runtime.js +125 -0
- package/dist/esm/plugin-state-runtime.js.map +1 -0
- package/dist/esm/state/runtime-script.d.ts +20 -0
- package/dist/esm/state/runtime-script.d.ts.map +1 -0
- package/dist/esm/state/runtime-script.js +28 -0
- package/dist/esm/state/runtime-script.js.map +1 -0
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/global-css.ts +67 -0
- package/src/index.ts +15 -0
- package/src/motion/contract.ts +78 -0
- package/src/motion/runtime-css.ts +95 -0
- package/src/motion/runtime-script.ts +630 -0
- package/src/motion/uidl-transform.ts +189 -0
- package/src/plugin-motion-runtime.ts +150 -0
- package/src/plugin-page-transition.ts +193 -0
- package/src/plugin-scroll-rail.ts +106 -0
- package/src/plugin-snap-into-view.ts +54 -0
- package/src/plugin-state-runtime.ts +80 -0
- package/src/state/runtime-script.ts +306 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { GeneratedFolder, ProjectUIDL } from '@teleporthq/teleport-types'
|
|
2
|
+
import uidlSample from '../../../../examples/uidl-samples/tests.json'
|
|
3
|
+
import {
|
|
4
|
+
createHTMLProjectGenerator,
|
|
5
|
+
pluginHomeReplace,
|
|
6
|
+
pluginStateRuntime,
|
|
7
|
+
ProjectPluginCloneGlobals,
|
|
8
|
+
} from '../../src'
|
|
9
|
+
import HTMLTemplate from '../../src/project-template'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A home page the way the editor exports its interactive library pieces: a
|
|
13
|
+
* Navigation component whose burger toggles its own menu, used twice (the
|
|
14
|
+
* first instance holds slot content that reads the PAGE's state), a tab set
|
|
15
|
+
* wired by the editor's click workflows with a state class on every trigger,
|
|
16
|
+
* and a monthly / yearly price switch.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const s = (content: unknown) => ({ type: 'static', content })
|
|
20
|
+
const state = (id: string) => ({ type: 'dynamic', content: { referenceType: 'state', id } })
|
|
21
|
+
const el = (elementType: string, content: Record<string, unknown> = {}) => ({
|
|
22
|
+
type: 'element',
|
|
23
|
+
content: { elementType, ...content },
|
|
24
|
+
})
|
|
25
|
+
const text = (id: string, label: string) =>
|
|
26
|
+
el('text', { semanticType: 'span', attrs: { id: s(id) }, children: [s(label)] })
|
|
27
|
+
const when = (id: string, operation: string, operand: unknown, node: unknown) => ({
|
|
28
|
+
type: 'conditional',
|
|
29
|
+
content: { reference: state(id), node, condition: { conditions: [{ operation, operand }] } },
|
|
30
|
+
})
|
|
31
|
+
const styles = (map: Record<string, string>) =>
|
|
32
|
+
Object.fromEntries(Object.entries(map).map(([key, value]) => [key, s(value)]))
|
|
33
|
+
|
|
34
|
+
const NAVIGATION = {
|
|
35
|
+
name: 'Navigation',
|
|
36
|
+
stateDefinitions: { menuOpen: { type: 'boolean', defaultValue: false } },
|
|
37
|
+
node: el('container', {
|
|
38
|
+
name: 'bar',
|
|
39
|
+
style: styles({ display: 'flex', gap: '16px', padding: '16px', alignItems: 'center' }),
|
|
40
|
+
children: [
|
|
41
|
+
el('container', {
|
|
42
|
+
name: 'burger',
|
|
43
|
+
attrs: { id: s('burger'), 'aria-label': s('Menu') },
|
|
44
|
+
style: styles({ width: '48px', height: '48px', background: '#ddd' }),
|
|
45
|
+
events: { click: [{ type: 'stateChange', modifies: 'menuOpen', newState: '$toggle' }] },
|
|
46
|
+
children: [
|
|
47
|
+
when('menuOpen', '===', false, text('burger-open', 'Open')),
|
|
48
|
+
when('menuOpen', '===', true, text('burger-close', 'Close')),
|
|
49
|
+
],
|
|
50
|
+
}),
|
|
51
|
+
when(
|
|
52
|
+
'menuOpen',
|
|
53
|
+
'===',
|
|
54
|
+
true,
|
|
55
|
+
el('container', {
|
|
56
|
+
name: 'menu',
|
|
57
|
+
attrs: { id: s('menu') },
|
|
58
|
+
style: styles({ display: 'flex', gap: '8px' }),
|
|
59
|
+
children: [text('menu-home', 'Home'), text('menu-about', 'About')],
|
|
60
|
+
})
|
|
61
|
+
),
|
|
62
|
+
{ type: 'slot', content: {} },
|
|
63
|
+
],
|
|
64
|
+
}),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const TAB_ACTIVE = 'tab-active'
|
|
68
|
+
|
|
69
|
+
const tabTrigger = (index: number) =>
|
|
70
|
+
el('container', {
|
|
71
|
+
name: `tab-trigger-${index}`,
|
|
72
|
+
attrs: { id: s(`tab-${index}`), 'data-tab-index': s(String(index)) },
|
|
73
|
+
style: styles({ padding: '12px 24px', cursor: 'pointer' }),
|
|
74
|
+
referencedStyles: {
|
|
75
|
+
[`active-${index}`]: {
|
|
76
|
+
type: 'style-map',
|
|
77
|
+
content: {
|
|
78
|
+
mapType: 'project-referenced',
|
|
79
|
+
referenceId: TAB_ACTIVE,
|
|
80
|
+
condition: {
|
|
81
|
+
reference: state('activeTabIndex'),
|
|
82
|
+
expression: { conditions: [{ operation: '=', operand: index }] },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
children: [s(`Tab ${index + 1}`)],
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const tabPanel = (index: number) =>
|
|
91
|
+
when(
|
|
92
|
+
'activeTabIndex',
|
|
93
|
+
'=',
|
|
94
|
+
index,
|
|
95
|
+
el('container', {
|
|
96
|
+
name: `tab-panel-${index}`,
|
|
97
|
+
attrs: { id: s(`panel-${index}`) },
|
|
98
|
+
style: styles({ display: 'flex', padding: '24px', minHeight: '120px' }),
|
|
99
|
+
children: [s(`Panel ${index + 1}`)],
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const tabWorkflow = (index: number) => ({
|
|
104
|
+
id: `switch-tab-${index}`,
|
|
105
|
+
name: `Switch to Tabs Tab ${index + 1}`,
|
|
106
|
+
trigger: {
|
|
107
|
+
nodeId: `trigger-${index}`,
|
|
108
|
+
type: 'event-element-clicked',
|
|
109
|
+
scope: 'element',
|
|
110
|
+
config: { nodeId: `doc-${index}`, elementHtmlId: `tab-${index}`, preventDefault: false },
|
|
111
|
+
},
|
|
112
|
+
nodes: [
|
|
113
|
+
{
|
|
114
|
+
id: `set-${index}`,
|
|
115
|
+
type: 'state-update-local-state',
|
|
116
|
+
label: 'Set Local State',
|
|
117
|
+
config: { property: 'activeTabIndex', value: index },
|
|
118
|
+
executionEnv: 'client',
|
|
119
|
+
stepNumber: 1,
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
edges: [{ id: `edge-${index}`, source: `trigger-${index}`, target: `set-${index}` }],
|
|
123
|
+
errorHandler: {
|
|
124
|
+
nodeId: `error-${index}`,
|
|
125
|
+
type: 'event-workflow-error',
|
|
126
|
+
config: { isErrorHandler: true },
|
|
127
|
+
executionEnv: 'client',
|
|
128
|
+
},
|
|
129
|
+
usedInNodes: { [`doc-${index}`]: true },
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const HOME_CHILDREN = [
|
|
133
|
+
el('component', {
|
|
134
|
+
semanticType: 'Navigation',
|
|
135
|
+
dependency: { type: 'local' },
|
|
136
|
+
children: [when('yearly', '===', true, text('slot-badge', 'Yearly billing'))],
|
|
137
|
+
}),
|
|
138
|
+
el('component', { semanticType: 'Navigation', dependency: { type: 'local' } }),
|
|
139
|
+
el('container', {
|
|
140
|
+
name: 'tabs',
|
|
141
|
+
attrs: { id: s('tabs') },
|
|
142
|
+
style: styles({ display: 'flex' }),
|
|
143
|
+
children: [0, 1, 2].map(tabTrigger),
|
|
144
|
+
}),
|
|
145
|
+
...[0, 1, 2].map(tabPanel),
|
|
146
|
+
el('button', {
|
|
147
|
+
name: 'billing-switch',
|
|
148
|
+
attrs: { id: s('billing-switch') },
|
|
149
|
+
events: { click: [{ type: 'stateChange', modifies: 'yearly', newState: '$toggle' }] },
|
|
150
|
+
children: [s('Bill yearly')],
|
|
151
|
+
}),
|
|
152
|
+
when('yearly', '===', false, text('price-monthly', '$19 / month')),
|
|
153
|
+
when('yearly', '===', true, text('price-yearly', '$190 / year')),
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
const sample = () => JSON.parse(JSON.stringify(uidlSample))
|
|
157
|
+
|
|
158
|
+
export const plainSite = (): ProjectUIDL => sample()
|
|
159
|
+
|
|
160
|
+
export const stateSite = (): ProjectUIDL => {
|
|
161
|
+
const uidl = sample()
|
|
162
|
+
uidl.root.styleSetDefinitions = {
|
|
163
|
+
...(uidl.root.styleSetDefinitions || {}),
|
|
164
|
+
[TAB_ACTIVE]: {
|
|
165
|
+
type: 'reusable-project-style-map',
|
|
166
|
+
content: styles({ color: '#c00', borderBottom: '2px solid #c00' }),
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
uidl.components.Navigation = NAVIGATION
|
|
170
|
+
uidl.workflows = {
|
|
171
|
+
workflows: Object.fromEntries(
|
|
172
|
+
[0, 1, 2].map((index) => [`switch-tab-${index}`, tabWorkflow(index)])
|
|
173
|
+
),
|
|
174
|
+
}
|
|
175
|
+
const homeRoute = uidl.root.stateDefinitions.route.values.find(
|
|
176
|
+
(route: { value: string }) => route.value === uidl.root.stateDefinitions.route.defaultValue
|
|
177
|
+
)
|
|
178
|
+
homeRoute.pageOptions = {
|
|
179
|
+
...(homeRoute.pageOptions || {}),
|
|
180
|
+
stateDefinitions: {
|
|
181
|
+
activeTabIndex: { type: 'number', defaultValue: 0 },
|
|
182
|
+
yearly: { type: 'boolean', defaultValue: false },
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
uidl.root.node.content.children[0].content.node.content.children = HOME_CHILDREN
|
|
186
|
+
return uidl
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const generateSite = async (
|
|
190
|
+
uidl: ProjectUIDL,
|
|
191
|
+
{ strictHtmlWhitespaceSensitivity = false } = {}
|
|
192
|
+
): Promise<GeneratedFolder> => {
|
|
193
|
+
if (strictHtmlWhitespaceSensitivity) {
|
|
194
|
+
// custom head code that ends in a script makes Prettier close the head as `</head\n >`
|
|
195
|
+
uidl.globals.customCode = { head: '<script>\n window.tqHeadReady = true\n</script>' }
|
|
196
|
+
}
|
|
197
|
+
const generator = createHTMLProjectGenerator()
|
|
198
|
+
generator.addPlugin(pluginHomeReplace)
|
|
199
|
+
generator.addPlugin(new ProjectPluginCloneGlobals({ strictHtmlWhitespaceSensitivity }))
|
|
200
|
+
generator.addPlugin(pluginStateRuntime)
|
|
201
|
+
return generator.generateProject(
|
|
202
|
+
uidl as unknown as Record<string, unknown>,
|
|
203
|
+
HTMLTemplate,
|
|
204
|
+
{},
|
|
205
|
+
strictHtmlWhitespaceSensitivity
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const fileOf = (folder: GeneratedFolder, name: string, fileType: string) =>
|
|
210
|
+
folder.files.find((file) => file.name === name && file.fileType === fileType)?.content ?? ''
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { FileType, ProjectUIDL } from '@teleporthq/teleport-types'
|
|
2
|
+
import uidlSample from '../../../../examples/uidl-samples/tests.json'
|
|
3
|
+
import { createHTMLProjectGenerator, pluginCloneGlobals, pluginHomeReplace } from '../../src'
|
|
4
|
+
import HTMLTemplate from '../../src/project-template'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An element shown on a condition appears as the page first loads in the
|
|
8
|
+
* Next.js export: the condition is resolved against the value its reference
|
|
9
|
+
* starts with. Until 2026-09-18 every STATE condition rendered nothing, true
|
|
10
|
+
* or not, so a download lost the burger of a closed menu and the first tab of
|
|
11
|
+
* every tab set. Since 2026-10-04 a condition on the page's own state keeps
|
|
12
|
+
* every branch in the page for the state runtime to switch, and the ones the
|
|
13
|
+
* page does not start on are `hidden` (state-runtime/ covers the runtime); a
|
|
14
|
+
* condition on anything else is still resolved once.
|
|
15
|
+
*/
|
|
16
|
+
const s = (content: unknown) => ({ type: 'static', content })
|
|
17
|
+
const span = (id: string) => ({
|
|
18
|
+
type: 'element',
|
|
19
|
+
content: { elementType: 'text', semanticType: 'span', attrs: { id: s(id) }, children: [s(id)] },
|
|
20
|
+
})
|
|
21
|
+
const when = (
|
|
22
|
+
reference: Record<string, unknown>,
|
|
23
|
+
conditions: Array<Record<string, unknown>>,
|
|
24
|
+
id: string
|
|
25
|
+
) => ({
|
|
26
|
+
type: 'conditional',
|
|
27
|
+
content: { reference, node: span(id), condition: { conditions, matchingCriteria: 'all' } },
|
|
28
|
+
})
|
|
29
|
+
const state = (id: string, refPath?: string[]) => ({
|
|
30
|
+
type: 'dynamic',
|
|
31
|
+
content: { referenceType: 'state', id, ...(refPath ? { refPath } : {}) },
|
|
32
|
+
})
|
|
33
|
+
const prop = (id: string) => ({ type: 'dynamic', content: { referenceType: 'prop', id } })
|
|
34
|
+
|
|
35
|
+
const homePage = async (
|
|
36
|
+
children: unknown[],
|
|
37
|
+
stateDefinitions: Record<string, unknown>,
|
|
38
|
+
propDefinitions: Record<string, unknown> = {}
|
|
39
|
+
): Promise<string> => {
|
|
40
|
+
const uidl = JSON.parse(JSON.stringify(uidlSample))
|
|
41
|
+
const home = uidl.root.node.content.children[0].content.node.content
|
|
42
|
+
home.children = children
|
|
43
|
+
// A page's own state and props travel with its route, as the editor exports them.
|
|
44
|
+
const homeRoute = uidl.root.stateDefinitions.route.values.find(
|
|
45
|
+
(route: { value: string }) => route.value === uidl.root.stateDefinitions.route.defaultValue
|
|
46
|
+
)
|
|
47
|
+
homeRoute.pageOptions = { ...(homeRoute.pageOptions || {}), stateDefinitions, propDefinitions }
|
|
48
|
+
const generator = createHTMLProjectGenerator()
|
|
49
|
+
generator.addPlugin(pluginHomeReplace)
|
|
50
|
+
generator.addPlugin(pluginCloneGlobals)
|
|
51
|
+
const { files } = await generator.generateProject(uidl as ProjectUIDL, HTMLTemplate)
|
|
52
|
+
return (
|
|
53
|
+
files.find((file) => file.name === 'index' && file.fileType === FileType.HTML)?.content ?? ''
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const openingTag = (page: string, id: string) =>
|
|
58
|
+
new RegExp(`<[a-z][^>]*\\bid="${id}"[^>]*>`).exec(page)?.[0]
|
|
59
|
+
const inPage = (page: string, id: string) => openingTag(page, id) !== undefined
|
|
60
|
+
/** In the markup and not `hidden`: what a visitor sees before any script runs. */
|
|
61
|
+
const shows = (page: string, id: string) =>
|
|
62
|
+
inPage(page, id) && !/\shidden(=""|\s|>)/.test(openingTag(page, id) ?? '')
|
|
63
|
+
|
|
64
|
+
describe('Conditions in a static HTML page', () => {
|
|
65
|
+
it('a state condition shows what the page starts with, and keeps the rest hidden for the runtime', async () => {
|
|
66
|
+
const page = await homePage(
|
|
67
|
+
[
|
|
68
|
+
when(state('menuOpen'), [{ operation: '===', operand: true }], 'menu-panel'),
|
|
69
|
+
when(state('menuOpen'), [{ operation: '===', operand: false }], 'burger'),
|
|
70
|
+
when(state('tab'), [{ operation: '===', operand: 'a' }], 'tab-a'),
|
|
71
|
+
when(state('tab'), [{ operation: '===', operand: 'b' }], 'tab-b'),
|
|
72
|
+
],
|
|
73
|
+
{
|
|
74
|
+
menuOpen: { type: 'boolean', defaultValue: false },
|
|
75
|
+
tab: { type: 'string', defaultValue: 'a' },
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
expect(shows(page, 'burger')).toBe(true)
|
|
80
|
+
expect(shows(page, 'tab-a')).toBe(true)
|
|
81
|
+
expect(shows(page, 'menu-panel')).toBe(false)
|
|
82
|
+
expect(shows(page, 'tab-b')).toBe(false)
|
|
83
|
+
expect(inPage(page, 'menu-panel')).toBe(true)
|
|
84
|
+
expect(inPage(page, 'tab-b')).toBe(true)
|
|
85
|
+
expect(openingTag(page, 'menu-panel')).toContain('data-tq-if="menuOpen"')
|
|
86
|
+
expect(openingTag(page, 'burger')).toContain('data-tq-if="!menuOpen"')
|
|
87
|
+
expect(openingTag(page, 'tab-b')).toContain(`data-tq-if="tab === 'b'"`)
|
|
88
|
+
expect(page).toContain(`data-tq-state="menuOpen = false; tab = 'a'"`)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('reads into an object state and compares numbers, once: the runtime keeps no objects', async () => {
|
|
92
|
+
const page = await homePage(
|
|
93
|
+
[
|
|
94
|
+
when(state('cart', ['count']), [{ operation: '>', operand: 0 }], 'cart-badge'),
|
|
95
|
+
when(state('cart', ['count']), [{ operation: '===', operand: 0 }], 'cart-empty'),
|
|
96
|
+
],
|
|
97
|
+
{ cart: { type: 'object', defaultValue: { count: 0 } } }
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
expect(shows(page, 'cart-empty')).toBe(true)
|
|
101
|
+
expect(inPage(page, 'cart-badge')).toBe(false)
|
|
102
|
+
expect(page).not.toContain('data-tq-')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('a condition with no operand tests the value itself, as the JSX generators do', async () => {
|
|
106
|
+
const page = await homePage(
|
|
107
|
+
[
|
|
108
|
+
when(state('loading'), [{ operation: '!' }], 'content'),
|
|
109
|
+
when(prop('isPromo'), [{ operation: '!' }], 'regular-price'),
|
|
110
|
+
],
|
|
111
|
+
{ loading: { type: 'boolean', defaultValue: true } },
|
|
112
|
+
{ isPromo: { type: 'boolean', defaultValue: false } }
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// loading starts true: "not loading" is false, and the runtime can turn it on
|
|
116
|
+
expect(shows(page, 'content')).toBe(false)
|
|
117
|
+
expect(openingTag(page, 'content')).toContain('data-tq-if="!loading"')
|
|
118
|
+
// isPromo starts false: "not promo" is true; a prop never changes in a static page
|
|
119
|
+
expect(shows(page, 'regular-price')).toBe(true)
|
|
120
|
+
expect(openingTag(page, 'regular-price')).not.toContain('data-tq-if')
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('a condition on a list or an object is answered on what it starts with', async () => {
|
|
124
|
+
const page = await homePage(
|
|
125
|
+
[
|
|
126
|
+
when(state('lines'), [{ operation: 'isEmpty' }], 'cart-empty'),
|
|
127
|
+
when(state('lines'), [{ operation: 'isNotEmpty' }], 'cart-lines'),
|
|
128
|
+
when(state('picked'), [{ operation: 'isNotEmpty' }], 'some-picked'),
|
|
129
|
+
when(state('picked'), [{ operation: 'contains', operand: 'b' }], 'picked-b'),
|
|
130
|
+
when(state('picked'), [{ operation: 'notContains', operand: 'z' }], 'not-picked-z'),
|
|
131
|
+
when(state('picked'), [{ operation: 'contains', operand: 'z' }], 'picked-z'),
|
|
132
|
+
when(state('picked'), [{ operation: 'lengthGreaterThan', operand: 1 }], 'several'),
|
|
133
|
+
when(state('picked'), [{ operation: 'lengthEquals', operand: 3 }], 'three'),
|
|
134
|
+
when(
|
|
135
|
+
state('rows'),
|
|
136
|
+
[{ operation: 'contains', operand: 7, containsField: 'id' }],
|
|
137
|
+
'row-seven'
|
|
138
|
+
),
|
|
139
|
+
when(state('filters'), [{ operation: 'hasKey', operand: 'size' }], 'by-size'),
|
|
140
|
+
when(state('filters'), [{ operation: 'notHasKey', operand: 'size' }], 'any-size'),
|
|
141
|
+
when(state('heading'), [{ operation: 'isNotEmpty' }], 'with-heading'),
|
|
142
|
+
],
|
|
143
|
+
{
|
|
144
|
+
lines: { type: 'array', defaultValue: [] },
|
|
145
|
+
picked: { type: 'array', defaultValue: ['a', 'b'] },
|
|
146
|
+
rows: { type: 'array', defaultValue: [{ id: 7 }, { id: 9 }] },
|
|
147
|
+
filters: { type: 'object', defaultValue: { size: 'm' } },
|
|
148
|
+
heading: { type: 'string', defaultValue: 'Hello' },
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
const shown = [
|
|
153
|
+
'cart-empty',
|
|
154
|
+
'some-picked',
|
|
155
|
+
'picked-b',
|
|
156
|
+
'not-picked-z',
|
|
157
|
+
'several',
|
|
158
|
+
'row-seven',
|
|
159
|
+
'by-size',
|
|
160
|
+
'with-heading',
|
|
161
|
+
]
|
|
162
|
+
const absent = ['cart-lines', 'picked-z', 'three', 'any-size']
|
|
163
|
+
expect(shown.filter((id) => !shows(page, id))).toEqual([])
|
|
164
|
+
expect(absent.filter((id) => inPage(page, id))).toEqual([])
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('a condition it cannot answer renders nothing, and the page is still exported', async () => {
|
|
168
|
+
const page = await homePage(
|
|
169
|
+
[
|
|
170
|
+
// a list is not a value `===` can be written against
|
|
171
|
+
when(state('lines'), [{ operation: '===', operand: 'x' }], 'list-equals'),
|
|
172
|
+
// the other side is read while the page runs
|
|
173
|
+
when(state('picked'), [{ operation: 'contains', operand: state('choice') }], 'by-choice'),
|
|
174
|
+
when(state('filters'), [{ operation: '!' }], 'no-filters'),
|
|
175
|
+
when(state('open'), [{ operation: '===', operand: true }], 'panel'),
|
|
176
|
+
],
|
|
177
|
+
{
|
|
178
|
+
lines: { type: 'array', defaultValue: [] },
|
|
179
|
+
picked: { type: 'array', defaultValue: ['a'] },
|
|
180
|
+
filters: { type: 'object', defaultValue: {} },
|
|
181
|
+
choice: { type: 'string', defaultValue: 'a' },
|
|
182
|
+
open: { type: 'boolean', defaultValue: true },
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
expect(['list-equals', 'by-choice', 'no-filters'].filter((id) => inPage(page, id))).toEqual([])
|
|
187
|
+
expect(shows(page, 'panel')).toBe(true)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('a value holding a quote still compares', async () => {
|
|
191
|
+
const quote = 'She said "yes"'
|
|
192
|
+
const page = await homePage(
|
|
193
|
+
[when(state('answer'), [{ operation: '===', operand: quote }], 'quoted')],
|
|
194
|
+
{ answer: { type: 'string', defaultValue: quote } }
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
expect(shows(page, 'quoted')).toBe(true)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('a state with no starting value shows nothing that depends on it', async () => {
|
|
201
|
+
const page = await homePage(
|
|
202
|
+
[when(state('unknown'), [{ operation: '===', operand: true }], 'depends')],
|
|
203
|
+
{}
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
expect(inPage(page, 'depends')).toBe(false)
|
|
207
|
+
})
|
|
208
|
+
})
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import uidlSample from '../../../../examples/uidl-samples/tests.json'
|
|
2
2
|
import invalidUidlSample from '../../../../examples/test-samples/project-invalid-sample.json'
|
|
3
3
|
import projectWithSlot from '../../../../examples/test-samples/project-with-slot.json'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
createHTMLProjectGenerator,
|
|
6
|
+
pluginCloneGlobals,
|
|
7
|
+
pluginHomeReplace,
|
|
8
|
+
pluginSnapIntoView,
|
|
9
|
+
pluginScrollRail,
|
|
10
|
+
} from '../../src'
|
|
5
11
|
import HTMLTemplate from '../../src/project-template'
|
|
6
12
|
import { FileType } from '@teleporthq/teleport-types'
|
|
7
13
|
|
|
@@ -72,6 +78,73 @@ describe('Html Project Generator', () => {
|
|
|
72
78
|
expect(indexFile?.content).toContain(`This is amazing, because this is a named-slot`)
|
|
73
79
|
})
|
|
74
80
|
|
|
81
|
+
it('Snap into view: the native snap rule ships only when an element opts in', async () => {
|
|
82
|
+
const plain = createHTMLProjectGenerator()
|
|
83
|
+
plain.addPlugin(pluginSnapIntoView)
|
|
84
|
+
const untouched = await plain.generateProject(uidlSample, HTMLTemplate)
|
|
85
|
+
untouched.files.forEach((file) => expect(file.content).not.toContain('scroll-snap-type'))
|
|
86
|
+
|
|
87
|
+
const uidl = JSON.parse(JSON.stringify(uidlSample))
|
|
88
|
+
const about = uidl.root.node.content.children.find(
|
|
89
|
+
(child: { content: { value?: string } }) => child.content.value === 'About'
|
|
90
|
+
)
|
|
91
|
+
about.content.node.content.attrs = {
|
|
92
|
+
...(about.content.node.content.attrs || {}),
|
|
93
|
+
'data-snap-into-view': { type: 'static', content: 'firm' },
|
|
94
|
+
}
|
|
95
|
+
const generator = createHTMLProjectGenerator()
|
|
96
|
+
generator.addPlugin(pluginSnapIntoView)
|
|
97
|
+
const snapped = await generator.generateProject(uidl, HTMLTemplate)
|
|
98
|
+
|
|
99
|
+
const globalCss = snapped.files.find(
|
|
100
|
+
(file) => file.name === 'style' && file.fileType === FileType.CSS
|
|
101
|
+
)
|
|
102
|
+
const aboutPage = snapped.files.find(
|
|
103
|
+
(file) => file.name === 'about' && file.fileType === FileType.HTML
|
|
104
|
+
)
|
|
105
|
+
const rulesCarrier = globalCss ? globalCss.content : aboutPage?.content || ''
|
|
106
|
+
expect(rulesCarrier).toContain('scroll-snap-type: y proximity')
|
|
107
|
+
expect(rulesCarrier).toContain('[data-snap-into-view="firm"]')
|
|
108
|
+
expect(rulesCarrier).toContain('[data-snap-into-view="gentle"]')
|
|
109
|
+
expect(rulesCarrier).not.toContain('mandatory')
|
|
110
|
+
expect(aboutPage?.content).toContain('data-snap-into-view="firm"')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('Scroll rail: CSS in the global stylesheet, the wheel script per page, only when used', async () => {
|
|
114
|
+
const plain = createHTMLProjectGenerator()
|
|
115
|
+
plain.addPlugin(pluginScrollRail)
|
|
116
|
+
const untouched = await plain.generateProject(uidlSample, HTMLTemplate)
|
|
117
|
+
untouched.files.forEach((file) => {
|
|
118
|
+
expect(file.content).not.toContain('scroll-snap-type: x')
|
|
119
|
+
expect(file.content).not.toContain('data-scroll-rail-wheel')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
const uidl = JSON.parse(JSON.stringify(uidlSample))
|
|
123
|
+
const about = uidl.root.node.content.children.find(
|
|
124
|
+
(child: { content: { value?: string } }) => child.content.value === 'About'
|
|
125
|
+
)
|
|
126
|
+
about.content.node.content.attrs = {
|
|
127
|
+
...(about.content.node.content.attrs || {}),
|
|
128
|
+
'data-scroll-rail-snap': { type: 'static', content: 'gentle' },
|
|
129
|
+
'data-scroll-rail-wheel': { type: 'static', content: 'true' },
|
|
130
|
+
}
|
|
131
|
+
const generator = createHTMLProjectGenerator()
|
|
132
|
+
generator.addPlugin(pluginScrollRail)
|
|
133
|
+
const railed = await generator.generateProject(uidl, HTMLTemplate)
|
|
134
|
+
|
|
135
|
+
const globalCss = railed.files.find(
|
|
136
|
+
(file) => file.name === 'style' && file.fileType === FileType.CSS
|
|
137
|
+
)
|
|
138
|
+
const aboutPage = railed.files.find(
|
|
139
|
+
(file) => file.name === 'about' && file.fileType === FileType.HTML
|
|
140
|
+
)
|
|
141
|
+
const rulesCarrier = globalCss ? globalCss.content : aboutPage?.content || ''
|
|
142
|
+
expect(rulesCarrier).toContain('scroll-snap-type: x proximity')
|
|
143
|
+
expect(rulesCarrier).toContain('scroll-snap-align: start')
|
|
144
|
+
expect(aboutPage?.content).toContain('data-scroll-rail-snap="gentle"')
|
|
145
|
+
expect(aboutPage?.content).toContain("addEventListener('wheel'")
|
|
146
|
+
})
|
|
147
|
+
|
|
75
148
|
it('throws error when invalid UIDL sample is used', async () => {
|
|
76
149
|
const generator = createHTMLProjectGenerator()
|
|
77
150
|
const result = generator.generateProject(invalidUidlSample, HTMLTemplate)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { FileType, GeneratedFile, ProjectPluginStructure } from '@teleporthq/teleport-types'
|
|
2
|
+
import { appendGlobalCss, appendPageScript } from '../../src/global-css'
|
|
3
|
+
|
|
4
|
+
// A page as Prettier writes it with strict whitespace (the editor's "strict
|
|
5
|
+
// white spacing for HTML" setting): no closing tag stands on its own.
|
|
6
|
+
const STRICT_PAGE = [
|
|
7
|
+
'<!DOCTYPE html>',
|
|
8
|
+
'<html lang="en"',
|
|
9
|
+
' ><head',
|
|
10
|
+
' ><title>Home</title',
|
|
11
|
+
' ></head',
|
|
12
|
+
' ><body',
|
|
13
|
+
' ><div>Content</div',
|
|
14
|
+
' ></body',
|
|
15
|
+
'></html>',
|
|
16
|
+
].join('\n')
|
|
17
|
+
|
|
18
|
+
const structureWith = (page: string): ProjectPluginStructure =>
|
|
19
|
+
({
|
|
20
|
+
files: new Map([
|
|
21
|
+
['index', { path: [''], files: [{ name: 'index', fileType: FileType.HTML, content: page }] }],
|
|
22
|
+
]),
|
|
23
|
+
} as unknown as ProjectPluginStructure)
|
|
24
|
+
|
|
25
|
+
const pageOf = (structure: ProjectPluginStructure): string =>
|
|
26
|
+
(structure.files.get('index').files as GeneratedFile[])[0].content
|
|
27
|
+
|
|
28
|
+
describe('Global CSS and page scripts in a page whose tags close the strict way', () => {
|
|
29
|
+
it('a project without a stylesheet carries the CSS inside the head, after the doctype', () => {
|
|
30
|
+
const structure = structureWith(STRICT_PAGE)
|
|
31
|
+
appendGlobalCss(structure, '.rail { overflow: hidden; }\n')
|
|
32
|
+
const page = pageOf(structure)
|
|
33
|
+
|
|
34
|
+
expect(page.startsWith('<!DOCTYPE html>')).toBe(true)
|
|
35
|
+
expect(page.indexOf('<style>')).toBeGreaterThan(page.indexOf('<head'))
|
|
36
|
+
expect(page.indexOf('.rail { overflow: hidden; }')).toBeLessThan(page.search(/<\/head\s*>/))
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('a page script runs at the end of the body', () => {
|
|
40
|
+
const structure = structureWith(STRICT_PAGE)
|
|
41
|
+
appendPageScript(structure, 'window.ready = true')
|
|
42
|
+
const page = pageOf(structure)
|
|
43
|
+
|
|
44
|
+
expect(page.indexOf('window.ready = true')).toBeGreaterThan(page.indexOf('<div>Content</div'))
|
|
45
|
+
expect(page.indexOf('window.ready = true')).toBeLessThan(page.search(/<\/body\s*>/))
|
|
46
|
+
})
|
|
47
|
+
})
|