@teleporthq/teleport-project-generator-html 0.43.69 → 0.43.71

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.
Files changed (119) hide show
  1. package/__tests__/_helpers/state-site.ts +210 -0
  2. package/__tests__/conditionals/index.ts +141 -0
  3. package/__tests__/end2end/index.ts +74 -1
  4. package/__tests__/global-css/index.ts +47 -0
  5. package/__tests__/motion/index.ts +375 -0
  6. package/__tests__/page-transition/index.ts +356 -0
  7. package/__tests__/state-runtime/index.ts +174 -0
  8. package/__tests__/state-runtime/runtime.ts +225 -0
  9. package/dist/cjs/global-css.d.ts +22 -0
  10. package/dist/cjs/global-css.d.ts.map +1 -0
  11. package/dist/cjs/global-css.js +61 -0
  12. package/dist/cjs/global-css.js.map +1 -0
  13. package/dist/cjs/index.d.ts +6 -1
  14. package/dist/cjs/index.d.ts.map +1 -1
  15. package/dist/cjs/index.js +16 -1
  16. package/dist/cjs/index.js.map +1 -1
  17. package/dist/cjs/motion/contract.d.ts +31 -0
  18. package/dist/cjs/motion/contract.d.ts.map +1 -0
  19. package/dist/cjs/motion/contract.js +73 -0
  20. package/dist/cjs/motion/contract.js.map +1 -0
  21. package/dist/cjs/motion/runtime-css.d.ts +21 -0
  22. package/dist/cjs/motion/runtime-css.d.ts.map +1 -0
  23. package/dist/cjs/motion/runtime-css.js +95 -0
  24. package/dist/cjs/motion/runtime-css.js.map +1 -0
  25. package/dist/cjs/motion/runtime-script.d.ts +7 -0
  26. package/dist/cjs/motion/runtime-script.d.ts.map +1 -0
  27. package/dist/cjs/motion/runtime-script.js +52 -0
  28. package/dist/cjs/motion/runtime-script.js.map +1 -0
  29. package/dist/cjs/motion/uidl-transform.d.ts +22 -0
  30. package/dist/cjs/motion/uidl-transform.d.ts.map +1 -0
  31. package/dist/cjs/motion/uidl-transform.js +148 -0
  32. package/dist/cjs/motion/uidl-transform.js.map +1 -0
  33. package/dist/cjs/plugin-motion-runtime.d.ts +8 -0
  34. package/dist/cjs/plugin-motion-runtime.d.ts.map +1 -0
  35. package/dist/cjs/plugin-motion-runtime.js +178 -0
  36. package/dist/cjs/plugin-motion-runtime.js.map +1 -0
  37. package/dist/cjs/plugin-page-transition.d.ts +8 -0
  38. package/dist/cjs/plugin-page-transition.d.ts.map +1 -0
  39. package/dist/cjs/plugin-page-transition.js +187 -0
  40. package/dist/cjs/plugin-page-transition.js.map +1 -0
  41. package/dist/cjs/plugin-scroll-rail.d.ts +20 -0
  42. package/dist/cjs/plugin-scroll-rail.d.ts.map +1 -0
  43. package/dist/cjs/plugin-scroll-rail.js +107 -0
  44. package/dist/cjs/plugin-scroll-rail.js.map +1 -0
  45. package/dist/cjs/plugin-snap-into-view.d.ts +18 -0
  46. package/dist/cjs/plugin-snap-into-view.d.ts.map +1 -0
  47. package/dist/cjs/plugin-snap-into-view.js +99 -0
  48. package/dist/cjs/plugin-snap-into-view.js.map +1 -0
  49. package/dist/cjs/plugin-state-runtime.d.ts +7 -0
  50. package/dist/cjs/plugin-state-runtime.d.ts.map +1 -0
  51. package/dist/cjs/plugin-state-runtime.js +128 -0
  52. package/dist/cjs/plugin-state-runtime.js.map +1 -0
  53. package/dist/cjs/state/runtime-script.d.ts +20 -0
  54. package/dist/cjs/state/runtime-script.d.ts.map +1 -0
  55. package/dist/cjs/state/runtime-script.js +31 -0
  56. package/dist/cjs/state/runtime-script.js.map +1 -0
  57. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  58. package/dist/esm/global-css.d.ts +22 -0
  59. package/dist/esm/global-css.d.ts.map +1 -0
  60. package/dist/esm/global-css.js +55 -0
  61. package/dist/esm/global-css.js.map +1 -0
  62. package/dist/esm/index.d.ts +6 -1
  63. package/dist/esm/index.d.ts.map +1 -1
  64. package/dist/esm/index.js +6 -1
  65. package/dist/esm/index.js.map +1 -1
  66. package/dist/esm/motion/contract.d.ts +31 -0
  67. package/dist/esm/motion/contract.d.ts.map +1 -0
  68. package/dist/esm/motion/contract.js +69 -0
  69. package/dist/esm/motion/contract.js.map +1 -0
  70. package/dist/esm/motion/runtime-css.d.ts +21 -0
  71. package/dist/esm/motion/runtime-css.d.ts.map +1 -0
  72. package/dist/esm/motion/runtime-css.js +89 -0
  73. package/dist/esm/motion/runtime-css.js.map +1 -0
  74. package/dist/esm/motion/runtime-script.d.ts +7 -0
  75. package/dist/esm/motion/runtime-script.d.ts.map +1 -0
  76. package/dist/esm/motion/runtime-script.js +48 -0
  77. package/dist/esm/motion/runtime-script.js.map +1 -0
  78. package/dist/esm/motion/uidl-transform.d.ts +22 -0
  79. package/dist/esm/motion/uidl-transform.d.ts.map +1 -0
  80. package/dist/esm/motion/uidl-transform.js +144 -0
  81. package/dist/esm/motion/uidl-transform.js.map +1 -0
  82. package/dist/esm/plugin-motion-runtime.d.ts +8 -0
  83. package/dist/esm/plugin-motion-runtime.d.ts.map +1 -0
  84. package/dist/esm/plugin-motion-runtime.js +175 -0
  85. package/dist/esm/plugin-motion-runtime.js.map +1 -0
  86. package/dist/esm/plugin-page-transition.d.ts +8 -0
  87. package/dist/esm/plugin-page-transition.d.ts.map +1 -0
  88. package/dist/esm/plugin-page-transition.js +184 -0
  89. package/dist/esm/plugin-page-transition.js.map +1 -0
  90. package/dist/esm/plugin-scroll-rail.d.ts +20 -0
  91. package/dist/esm/plugin-scroll-rail.d.ts.map +1 -0
  92. package/dist/esm/plugin-scroll-rail.js +104 -0
  93. package/dist/esm/plugin-scroll-rail.js.map +1 -0
  94. package/dist/esm/plugin-snap-into-view.d.ts +18 -0
  95. package/dist/esm/plugin-snap-into-view.d.ts.map +1 -0
  96. package/dist/esm/plugin-snap-into-view.js +95 -0
  97. package/dist/esm/plugin-snap-into-view.js.map +1 -0
  98. package/dist/esm/plugin-state-runtime.d.ts +7 -0
  99. package/dist/esm/plugin-state-runtime.d.ts.map +1 -0
  100. package/dist/esm/plugin-state-runtime.js +125 -0
  101. package/dist/esm/plugin-state-runtime.js.map +1 -0
  102. package/dist/esm/state/runtime-script.d.ts +20 -0
  103. package/dist/esm/state/runtime-script.d.ts.map +1 -0
  104. package/dist/esm/state/runtime-script.js +28 -0
  105. package/dist/esm/state/runtime-script.js.map +1 -0
  106. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  107. package/package.json +8 -8
  108. package/src/global-css.ts +67 -0
  109. package/src/index.ts +15 -0
  110. package/src/motion/contract.ts +78 -0
  111. package/src/motion/runtime-css.ts +95 -0
  112. package/src/motion/runtime-script.ts +630 -0
  113. package/src/motion/uidl-transform.ts +189 -0
  114. package/src/plugin-motion-runtime.ts +150 -0
  115. package/src/plugin-page-transition.ts +193 -0
  116. package/src/plugin-scroll-rail.ts +106 -0
  117. package/src/plugin-snap-into-view.ts +54 -0
  118. package/src/plugin-state-runtime.ts +80 -0
  119. 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,141 @@
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 value holding a quote still compares', async () => {
124
+ const quote = 'She said "yes"'
125
+ const page = await homePage(
126
+ [when(state('answer'), [{ operation: '===', operand: quote }], 'quoted')],
127
+ { answer: { type: 'string', defaultValue: quote } }
128
+ )
129
+
130
+ expect(shows(page, 'quoted')).toBe(true)
131
+ })
132
+
133
+ it('a state with no starting value shows nothing that depends on it', async () => {
134
+ const page = await homePage(
135
+ [when(state('unknown'), [{ operation: '===', operand: true }], 'depends')],
136
+ {}
137
+ )
138
+
139
+ expect(inPage(page, 'depends')).toBe(false)
140
+ })
141
+ })
@@ -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 { createHTMLProjectGenerator, pluginCloneGlobals, pluginHomeReplace } from '../../src'
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
+ })