@teleporthq/teleport-project-generator-next 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 (213) hide show
  1. package/__tests__/body-code-end2end.test.ts +71 -0
  2. package/__tests__/body-code-unpack.test.ts +101 -0
  3. package/__tests__/custom-property-styles.test.ts +37 -0
  4. package/__tests__/document-inline-script-guard.test.ts +58 -0
  5. package/__tests__/emitted-components-rules-of-hooks.test.ts +10 -1
  6. package/__tests__/motion-end2end.test.ts +70 -0
  7. package/__tests__/motion-stagger-repeater.test.ts +8 -4
  8. package/__tests__/page-transition-end2end.test.ts +619 -0
  9. package/__tests__/pagination-scroll-runtime.test.ts +9 -1
  10. package/__tests__/scroll-scene-chapter-index.test.ts +35 -0
  11. package/__tests__/scroll-scene-end2end.test.ts +292 -0
  12. package/__tests__/scroll-scene-moment.test.ts +46 -0
  13. package/__tests__/scroll-scene-points.test.ts +49 -0
  14. package/__tests__/scroll-video-end2end.test.ts +146 -0
  15. package/__tests__/widget-project-plugins.test.ts +24 -5
  16. package/dist/cjs/body-code/body-code-component.d.ts +37 -0
  17. package/dist/cjs/body-code/body-code-component.d.ts.map +1 -0
  18. package/dist/cjs/body-code/body-code-component.js +50 -0
  19. package/dist/cjs/body-code/body-code-component.js.map +1 -0
  20. package/dist/cjs/body-code/project-plugin.d.ts +11 -0
  21. package/dist/cjs/body-code/project-plugin.d.ts.map +1 -0
  22. package/dist/cjs/body-code/project-plugin.js +86 -0
  23. package/dist/cjs/body-code/project-plugin.js.map +1 -0
  24. package/dist/cjs/index.d.ts.map +1 -1
  25. package/dist/cjs/index.js +30 -22
  26. package/dist/cjs/index.js.map +1 -1
  27. package/dist/cjs/local-component-path-plugin.d.ts.map +1 -1
  28. package/dist/cjs/local-component-path-plugin.js +2 -0
  29. package/dist/cjs/local-component-path-plugin.js.map +1 -1
  30. package/dist/cjs/next-project-mapping.d.ts.map +1 -1
  31. package/dist/cjs/next-project-mapping.js +14 -0
  32. package/dist/cjs/next-project-mapping.js.map +1 -1
  33. package/dist/cjs/page-transition/page-transition-component.d.ts +55 -0
  34. package/dist/cjs/page-transition/page-transition-component.d.ts.map +1 -0
  35. package/dist/cjs/page-transition/page-transition-component.js +92 -0
  36. package/dist/cjs/page-transition/page-transition-component.js.map +1 -0
  37. package/dist/cjs/page-transition/page-transition-variants.d.ts +16 -0
  38. package/dist/cjs/page-transition/page-transition-variants.d.ts.map +1 -0
  39. package/dist/cjs/page-transition/page-transition-variants.js +14 -0
  40. package/dist/cjs/page-transition/page-transition-variants.js.map +1 -0
  41. package/dist/cjs/page-transition/project-plugin.d.ts +20 -0
  42. package/dist/cjs/page-transition/project-plugin.d.ts.map +1 -0
  43. package/dist/cjs/page-transition/project-plugin.js +118 -0
  44. package/dist/cjs/page-transition/project-plugin.js.map +1 -0
  45. package/dist/cjs/page-transition/reveal-on-screen.d.ts +36 -0
  46. package/dist/cjs/page-transition/reveal-on-screen.d.ts.map +1 -0
  47. package/dist/cjs/page-transition/reveal-on-screen.js +77 -0
  48. package/dist/cjs/page-transition/reveal-on-screen.js.map +1 -0
  49. package/dist/cjs/pagination-scroll/pagination-scroll-runtime.d.ts.map +1 -1
  50. package/dist/cjs/pagination-scroll/pagination-scroll-runtime.js +1 -1
  51. package/dist/cjs/pagination-scroll/pagination-scroll-runtime.js.map +1 -1
  52. package/dist/cjs/scroll-rail/project-plugin.d.ts +14 -0
  53. package/dist/cjs/scroll-rail/project-plugin.d.ts.map +1 -0
  54. package/dist/cjs/scroll-rail/project-plugin.js +98 -0
  55. package/dist/cjs/scroll-rail/project-plugin.js.map +1 -0
  56. package/dist/cjs/scroll-rail/scroll-rail-component.d.ts +21 -0
  57. package/dist/cjs/scroll-rail/scroll-rail-component.d.ts.map +1 -0
  58. package/dist/cjs/scroll-rail/scroll-rail-component.js +31 -0
  59. package/dist/cjs/scroll-rail/scroll-rail-component.js.map +1 -0
  60. package/dist/cjs/snap-into-view/project-plugin.d.ts +14 -0
  61. package/dist/cjs/snap-into-view/project-plugin.d.ts.map +1 -0
  62. package/dist/cjs/snap-into-view/project-plugin.js +101 -0
  63. package/dist/cjs/snap-into-view/project-plugin.js.map +1 -0
  64. package/dist/cjs/snap-into-view/snap-into-view-component.d.ts +16 -0
  65. package/dist/cjs/snap-into-view/snap-into-view-component.d.ts.map +1 -0
  66. package/dist/cjs/snap-into-view/snap-into-view-component.js +22 -0
  67. package/dist/cjs/snap-into-view/snap-into-view-component.js.map +1 -0
  68. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  69. package/dist/cjs/utils.d.ts.map +1 -1
  70. package/dist/cjs/utils.js +13 -1
  71. package/dist/cjs/utils.js.map +1 -1
  72. package/dist/cjs/widgets/index.d.ts.map +1 -1
  73. package/dist/cjs/widgets/index.js +18 -0
  74. package/dist/cjs/widgets/index.js.map +1 -1
  75. package/dist/cjs/widgets/motion-component.d.ts +4 -3
  76. package/dist/cjs/widgets/motion-component.d.ts.map +1 -1
  77. package/dist/cjs/widgets/motion-component.js +6 -4
  78. package/dist/cjs/widgets/motion-component.js.map +1 -1
  79. package/dist/cjs/widgets/scroll-scene-chapter-index.d.ts +3 -0
  80. package/dist/cjs/widgets/scroll-scene-chapter-index.d.ts.map +1 -0
  81. package/dist/cjs/widgets/scroll-scene-chapter-index.js +6 -0
  82. package/dist/cjs/widgets/scroll-scene-chapter-index.js.map +1 -0
  83. package/dist/cjs/widgets/scroll-scene-component.d.ts +20 -0
  84. package/dist/cjs/widgets/scroll-scene-component.d.ts.map +1 -0
  85. package/dist/cjs/widgets/scroll-scene-component.js +27 -0
  86. package/dist/cjs/widgets/scroll-scene-component.js.map +1 -0
  87. package/dist/cjs/widgets/scroll-scene-moment.d.ts +4 -0
  88. package/dist/cjs/widgets/scroll-scene-moment.d.ts.map +1 -0
  89. package/dist/cjs/widgets/scroll-scene-moment.js +6 -0
  90. package/dist/cjs/widgets/scroll-scene-moment.js.map +1 -0
  91. package/dist/cjs/widgets/scroll-scene-points.d.ts +5 -0
  92. package/dist/cjs/widgets/scroll-scene-points.d.ts.map +1 -0
  93. package/dist/cjs/widgets/scroll-scene-points.js +8 -0
  94. package/dist/cjs/widgets/scroll-scene-points.js.map +1 -0
  95. package/dist/cjs/widgets/scroll-scene-unclip.d.ts +3 -0
  96. package/dist/cjs/widgets/scroll-scene-unclip.d.ts.map +1 -0
  97. package/dist/cjs/widgets/scroll-scene-unclip.js +6 -0
  98. package/dist/cjs/widgets/scroll-scene-unclip.js.map +1 -0
  99. package/dist/cjs/widgets/scroll-video-component.d.ts +25 -0
  100. package/dist/cjs/widgets/scroll-video-component.d.ts.map +1 -0
  101. package/dist/cjs/widgets/scroll-video-component.js +32 -0
  102. package/dist/cjs/widgets/scroll-video-component.js.map +1 -0
  103. package/dist/esm/body-code/body-code-component.d.ts +37 -0
  104. package/dist/esm/body-code/body-code-component.d.ts.map +1 -0
  105. package/dist/esm/body-code/body-code-component.js +44 -0
  106. package/dist/esm/body-code/body-code-component.js.map +1 -0
  107. package/dist/esm/body-code/project-plugin.d.ts +11 -0
  108. package/dist/esm/body-code/project-plugin.d.ts.map +1 -0
  109. package/dist/esm/body-code/project-plugin.js +83 -0
  110. package/dist/esm/body-code/project-plugin.js.map +1 -0
  111. package/dist/esm/index.d.ts.map +1 -1
  112. package/dist/esm/index.js +8 -0
  113. package/dist/esm/index.js.map +1 -1
  114. package/dist/esm/local-component-path-plugin.d.ts.map +1 -1
  115. package/dist/esm/local-component-path-plugin.js +2 -0
  116. package/dist/esm/local-component-path-plugin.js.map +1 -1
  117. package/dist/esm/next-project-mapping.d.ts.map +1 -1
  118. package/dist/esm/next-project-mapping.js +14 -0
  119. package/dist/esm/next-project-mapping.js.map +1 -1
  120. package/dist/esm/page-transition/page-transition-component.d.ts +55 -0
  121. package/dist/esm/page-transition/page-transition-component.d.ts.map +1 -0
  122. package/dist/esm/page-transition/page-transition-component.js +88 -0
  123. package/dist/esm/page-transition/page-transition-component.js.map +1 -0
  124. package/dist/esm/page-transition/page-transition-variants.d.ts +16 -0
  125. package/dist/esm/page-transition/page-transition-variants.d.ts.map +1 -0
  126. package/dist/esm/page-transition/page-transition-variants.js +11 -0
  127. package/dist/esm/page-transition/page-transition-variants.js.map +1 -0
  128. package/dist/esm/page-transition/project-plugin.d.ts +20 -0
  129. package/dist/esm/page-transition/project-plugin.d.ts.map +1 -0
  130. package/dist/esm/page-transition/project-plugin.js +115 -0
  131. package/dist/esm/page-transition/project-plugin.js.map +1 -0
  132. package/dist/esm/page-transition/reveal-on-screen.d.ts +36 -0
  133. package/dist/esm/page-transition/reveal-on-screen.d.ts.map +1 -0
  134. package/dist/esm/page-transition/reveal-on-screen.js +73 -0
  135. package/dist/esm/page-transition/reveal-on-screen.js.map +1 -0
  136. package/dist/esm/pagination-scroll/pagination-scroll-runtime.d.ts.map +1 -1
  137. package/dist/esm/pagination-scroll/pagination-scroll-runtime.js +1 -1
  138. package/dist/esm/pagination-scroll/pagination-scroll-runtime.js.map +1 -1
  139. package/dist/esm/scroll-rail/project-plugin.d.ts +14 -0
  140. package/dist/esm/scroll-rail/project-plugin.d.ts.map +1 -0
  141. package/dist/esm/scroll-rail/project-plugin.js +94 -0
  142. package/dist/esm/scroll-rail/project-plugin.js.map +1 -0
  143. package/dist/esm/scroll-rail/scroll-rail-component.d.ts +21 -0
  144. package/dist/esm/scroll-rail/scroll-rail-component.d.ts.map +1 -0
  145. package/dist/esm/scroll-rail/scroll-rail-component.js +27 -0
  146. package/dist/esm/scroll-rail/scroll-rail-component.js.map +1 -0
  147. package/dist/esm/snap-into-view/project-plugin.d.ts +14 -0
  148. package/dist/esm/snap-into-view/project-plugin.d.ts.map +1 -0
  149. package/dist/esm/snap-into-view/project-plugin.js +97 -0
  150. package/dist/esm/snap-into-view/project-plugin.js.map +1 -0
  151. package/dist/esm/snap-into-view/snap-into-view-component.d.ts +16 -0
  152. package/dist/esm/snap-into-view/snap-into-view-component.d.ts.map +1 -0
  153. package/dist/esm/snap-into-view/snap-into-view-component.js +18 -0
  154. package/dist/esm/snap-into-view/snap-into-view-component.js.map +1 -0
  155. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  156. package/dist/esm/utils.d.ts.map +1 -1
  157. package/dist/esm/utils.js +14 -2
  158. package/dist/esm/utils.js.map +1 -1
  159. package/dist/esm/widgets/index.d.ts.map +1 -1
  160. package/dist/esm/widgets/index.js +18 -0
  161. package/dist/esm/widgets/index.js.map +1 -1
  162. package/dist/esm/widgets/motion-component.d.ts +4 -3
  163. package/dist/esm/widgets/motion-component.d.ts.map +1 -1
  164. package/dist/esm/widgets/motion-component.js +6 -4
  165. package/dist/esm/widgets/motion-component.js.map +1 -1
  166. package/dist/esm/widgets/scroll-scene-chapter-index.d.ts +3 -0
  167. package/dist/esm/widgets/scroll-scene-chapter-index.d.ts.map +1 -0
  168. package/dist/esm/widgets/scroll-scene-chapter-index.js +3 -0
  169. package/dist/esm/widgets/scroll-scene-chapter-index.js.map +1 -0
  170. package/dist/esm/widgets/scroll-scene-component.d.ts +20 -0
  171. package/dist/esm/widgets/scroll-scene-component.d.ts.map +1 -0
  172. package/dist/esm/widgets/scroll-scene-component.js +23 -0
  173. package/dist/esm/widgets/scroll-scene-component.js.map +1 -0
  174. package/dist/esm/widgets/scroll-scene-moment.d.ts +4 -0
  175. package/dist/esm/widgets/scroll-scene-moment.d.ts.map +1 -0
  176. package/dist/esm/widgets/scroll-scene-moment.js +3 -0
  177. package/dist/esm/widgets/scroll-scene-moment.js.map +1 -0
  178. package/dist/esm/widgets/scroll-scene-points.d.ts +5 -0
  179. package/dist/esm/widgets/scroll-scene-points.d.ts.map +1 -0
  180. package/dist/esm/widgets/scroll-scene-points.js +5 -0
  181. package/dist/esm/widgets/scroll-scene-points.js.map +1 -0
  182. package/dist/esm/widgets/scroll-scene-unclip.d.ts +3 -0
  183. package/dist/esm/widgets/scroll-scene-unclip.d.ts.map +1 -0
  184. package/dist/esm/widgets/scroll-scene-unclip.js +3 -0
  185. package/dist/esm/widgets/scroll-scene-unclip.js.map +1 -0
  186. package/dist/esm/widgets/scroll-video-component.d.ts +25 -0
  187. package/dist/esm/widgets/scroll-video-component.d.ts.map +1 -0
  188. package/dist/esm/widgets/scroll-video-component.js +28 -0
  189. package/dist/esm/widgets/scroll-video-component.js.map +1 -0
  190. package/package.json +19 -19
  191. package/src/body-code/body-code-component.ts +79 -0
  192. package/src/body-code/project-plugin.ts +35 -0
  193. package/src/index.ts +8 -0
  194. package/src/local-component-path-plugin.ts +2 -0
  195. package/src/next-project-mapping.ts +14 -0
  196. package/src/page-transition/page-transition-component.ts +479 -0
  197. package/src/page-transition/page-transition-variants.ts +18 -0
  198. package/src/page-transition/project-plugin.ts +73 -0
  199. package/src/page-transition/reveal-on-screen.ts +89 -0
  200. package/src/pagination-scroll/pagination-scroll-runtime.ts +15 -3
  201. package/src/scroll-rail/project-plugin.ts +48 -0
  202. package/src/scroll-rail/scroll-rail-component.ts +174 -0
  203. package/src/snap-into-view/project-plugin.ts +50 -0
  204. package/src/snap-into-view/snap-into-view-component.ts +110 -0
  205. package/src/utils.ts +15 -2
  206. package/src/widgets/index.ts +18 -0
  207. package/src/widgets/motion-component.ts +82 -135
  208. package/src/widgets/scroll-scene-chapter-index.ts +3 -0
  209. package/src/widgets/scroll-scene-component.ts +348 -0
  210. package/src/widgets/scroll-scene-moment.ts +4 -0
  211. package/src/widgets/scroll-scene-points.ts +5 -0
  212. package/src/widgets/scroll-scene-unclip.ts +3 -0
  213. package/src/widgets/scroll-video-component.ts +219 -0
@@ -0,0 +1,71 @@
1
+ import { GeneratedFolder, ProjectUIDL } from '@teleporthq/teleport-types'
2
+ import uidlSample from '../../../examples/test-samples/project-sample.json'
3
+ import { createNextProjectGenerator } from '../src'
4
+ import NextTemplate from '../src/project-template'
5
+ import {
6
+ BODY_CODE_TEMPLATE_ATTR,
7
+ splitBodyCustomCode,
8
+ wrapBodyCustomCode,
9
+ } from '../src/body-code/body-code-component'
10
+
11
+ const template = JSON.parse(JSON.stringify(NextTemplate)) as GeneratedFolder
12
+
13
+ const BODY_CODE = [
14
+ '<script src="https://unpkg.com/@teleporthq/teleport-custom-scripts"></script>',
15
+ '<noscript><img src="https://px.example/pixel" alt="" /></noscript>',
16
+ '<script>window.__patched = true</script>',
17
+ ].join('\n')
18
+
19
+ const buildUidlWithBodyCode = (body: string | undefined): ProjectUIDL => {
20
+ const uidl = JSON.parse(JSON.stringify(uidlSample)) as ProjectUIDL
21
+ uidl.globals.customCode = body === undefined ? undefined : { body }
22
+ return uidl
23
+ }
24
+
25
+ const findFile = (folder: GeneratedFolder, folderName: string, fileName: string) =>
26
+ folder.subFolders
27
+ .find((sub) => sub.name === folderName)
28
+ ?.files.find((file) => file.name === fileName)
29
+
30
+ describe('body custom code runs after hydration', () => {
31
+ it('keeps noscript live and makes everything else inert', () => {
32
+ const { noscript, deferred } = splitBodyCustomCode(BODY_CODE)
33
+ expect(noscript).toBe('<noscript><img src="https://px.example/pixel" alt="" /></noscript>')
34
+ expect(deferred).not.toContain('<noscript')
35
+ expect(deferred).toContain('teleport-custom-scripts')
36
+ expect(deferred).toContain('window.__patched')
37
+
38
+ const html = wrapBodyCustomCode(BODY_CODE)
39
+ expect(html.indexOf('<noscript')).toBeLessThan(html.indexOf('<template'))
40
+ expect(html).toContain(`<template ${BODY_CODE_TEMPLATE_ATTR}>`)
41
+ // a body made only of noscript ships no template at all
42
+ expect(wrapBodyCustomCode('<noscript>x</noscript>')).toBe('<noscript>x</noscript>')
43
+ })
44
+
45
+ describe('generated project', () => {
46
+ const generator = createNextProjectGenerator()
47
+
48
+ it('ships the code inert in _document and the unpacker as an _app sibling', async () => {
49
+ const output = await generator.generateProject(buildUidlWithBodyCode(BODY_CODE), template)
50
+
51
+ const document = findFile(output, 'pages', '_document')
52
+ expect(document?.content).toContain(`<template ${BODY_CODE_TEMPLATE_ATTR}>`)
53
+ expect(document?.content).toMatch(/<noscript><img[\s\S]*<template/)
54
+
55
+ const app = findFile(output, 'pages', '_app')
56
+ expect(app?.content).toContain("import TqBodyCode from '../components/tq-body-code'")
57
+ expect(app?.content).toContain('<TqBodyCode />')
58
+
59
+ const component = findFile(output, 'components', 'tq-body-code')
60
+ expect(component?.content).toContain('useEffect')
61
+ expect(component?.content).toContain(`template[${BODY_CODE_TEMPLATE_ATTR}]`)
62
+ expect(component?.content).toContain('script.async = false')
63
+ })
64
+
65
+ it('adds nothing when the project has no body custom code', async () => {
66
+ const output = await generator.generateProject(buildUidlWithBodyCode(undefined), template)
67
+ expect(findFile(output, 'pages', '_app')?.content).not.toContain('TqBodyCode')
68
+ expect(findFile(output, 'components', 'tq-body-code')).toBeUndefined()
69
+ })
70
+ })
71
+ })
@@ -0,0 +1,101 @@
1
+ import { resolve } from 'path'
2
+ import { BODY_CODE_UNPACK_SCRIPT, wrapBodyCustomCode } from '../src/body-code/body-code-component'
3
+
4
+ /**
5
+ * The unpacker run against a DOM: what it does to each kind of script in the
6
+ * project's body custom code. This repo's own jsdom 16 cannot load (see
7
+ * teleport-project-generator-html/__tests__/state-runtime/runtime.ts), so the
8
+ * editor checkout next to this one lends its jsdom.
9
+ *
10
+ * What a DOM cannot show is why an inline script names the document: Chromium
11
+ * reports no file for an error thrown by a script inserted after the page was
12
+ * parsed, and _document's inline script error guard knows the project's own
13
+ * scripts by the document's address. Measured over http with the built
14
+ * packages: without the name such an error reaches the app's own error
15
+ * listener in Chromium, with it the guard stops it (WebKit names the document
16
+ * either way).
17
+ */
18
+ const loadJSDOM = () => {
19
+ const candidates = ['jsdom', resolve(__dirname, '../../../../teleport-gui/node_modules/jsdom')]
20
+ for (const candidate of candidates) {
21
+ try {
22
+ // tslint:disable-next-line no-var-requires
23
+ return require(candidate).JSDOM
24
+ } catch (error) {
25
+ // the next candidate
26
+ }
27
+ }
28
+ return undefined
29
+ }
30
+ const JSDOM = loadJSDOM()
31
+ if (!JSDOM) {
32
+ // tslint:disable-next-line no-console
33
+ console.warn('body code unpack tests skipped: no jsdom loads (see the note in this file)')
34
+ }
35
+ const describeInDom = JSDOM ? describe : describe.skip
36
+
37
+ const PAGE = 'https://site.test/pricing'
38
+ const NAMED_AFTER_THE_PAGE = `\n//# sourceURL=${PAGE}`
39
+
40
+ const STRUCTURED_DATA = '{"@context":"https://schema.org","@type":"Organization","name":"Holm"}'
41
+
42
+ const BODY_CODE = [
43
+ '<script src="https://cdn.site.test/widget.js"></script>',
44
+ '<script>window.__order = (window.__order || []).concat("helper")</script>',
45
+ '<script type="module">window.__moduleWasWritten = true</script>',
46
+ `<script type="application/ld+json">${STRUCTURED_DATA}</script>`,
47
+ '<script type="text/template"><p>{{ name }}</p></script>',
48
+ '<script type="text/javascript">window.__order = window.__order.concat("widget setup")</script>',
49
+ '<div id="widget-root"></div>',
50
+ ].join('\n')
51
+
52
+ /** The page a visitor is on, with the body custom code shipped inert, then unpacked as TqBodyCode does. */
53
+ const unpackOn = (address: string) => {
54
+ const dom = new JSDOM(
55
+ `<!doctype html><html><body><main>Page</main>${wrapBodyCustomCode(BODY_CODE)}</body></html>`,
56
+ { url: address, runScripts: 'dangerously' }
57
+ )
58
+ dom.window.eval(BODY_CODE_UNPACK_SCRIPT)
59
+ const scripts = Array.from(dom.window.document.querySelectorAll('script')) as HTMLScriptElement[]
60
+ return { window: dom.window, scripts }
61
+ }
62
+
63
+ describeInDom('body custom code, unpacked after hydration', () => {
64
+ it('runs the inline scripts in the order they were written', () => {
65
+ const { window } = unpackOn(PAGE)
66
+
67
+ expect(window.__order).toEqual(['helper', 'widget setup'])
68
+ expect(window.document.querySelector('template')).toBeNull()
69
+ expect(window.document.querySelector('#widget-root')).not.toBeNull()
70
+ })
71
+
72
+ it('names the document as the source of every inline script that runs', () => {
73
+ const { scripts } = unpackOn(PAGE)
74
+ const [, helper, moduleScript, , , setup] = scripts
75
+
76
+ expect(helper.textContent).toBe(
77
+ `window.__order = (window.__order || []).concat("helper")${NAMED_AFTER_THE_PAGE}`
78
+ )
79
+ expect(moduleScript.textContent).toBe(`window.__moduleWasWritten = true${NAMED_AFTER_THE_PAGE}`)
80
+ expect(setup.textContent).toBe(
81
+ `window.__order = window.__order.concat("widget setup")${NAMED_AFTER_THE_PAGE}`
82
+ )
83
+ })
84
+
85
+ it('names the page without the part after #, as an error names it', () => {
86
+ const { scripts } = unpackOn(`${PAGE}#plans`)
87
+
88
+ expect(scripts[1].textContent?.endsWith(NAMED_AFTER_THE_PAGE)).toBe(true)
89
+ })
90
+
91
+ it('leaves an external script and the data blocks exactly as written', () => {
92
+ const { scripts } = unpackOn(PAGE)
93
+ const [external, , , structuredData, markupTemplate] = scripts
94
+
95
+ expect(external.getAttribute('src')).toBe('https://cdn.site.test/widget.js')
96
+ expect(external.textContent).toBe('')
97
+ expect(structuredData.textContent).toBe(STRUCTURED_DATA)
98
+ expect(JSON.parse(structuredData.textContent || '')).toMatchObject({ name: 'Holm' })
99
+ expect(markupTemplate.textContent).toBe('<p>{{ name }}</p>')
100
+ })
101
+ })
@@ -0,0 +1,37 @@
1
+ import { GeneratedFolder, ProjectUIDL } from '@teleporthq/teleport-types'
2
+ import uidlSample from '../../../examples/test-samples/project-sample.json'
3
+ import { createNextProjectGenerator } from '../src'
4
+ import NextTemplate from '../src/project-template'
5
+
6
+ const template = JSON.parse(JSON.stringify(NextTemplate)) as GeneratedFolder
7
+
8
+ /** Slider arrows and bullets are styled through Swiper's CSS variables written as element styles. */
9
+ describe('custom-property element styles', () => {
10
+ it('ships --swiper-* variables as written', async () => {
11
+ const uidl = JSON.parse(JSON.stringify(uidlSample)) as ProjectUIDL
12
+ const indexPage = (uidl.root.node.content.children || []).find(
13
+ (child) =>
14
+ child.type === 'conditional' && (child.content as { value?: string }).value === 'index'
15
+ )
16
+ const pageElement = (indexPage as { content: { node: { content: { children: unknown[] } } } })
17
+ .content.node.content
18
+ pageElement.children.push({
19
+ type: 'element',
20
+ content: {
21
+ elementType: 'container',
22
+ attrs: { 'data-thq': { type: 'static', content: 'slider-button-next' } },
23
+ style: {
24
+ '--swiper-navigation-color': { type: 'static', content: '#ff0000' },
25
+ '--swiper-navigation-size': { type: 'static', content: '24px' },
26
+ },
27
+ children: [],
28
+ },
29
+ })
30
+ const output = await createNextProjectGenerator().generateProject(uidl, template)
31
+ const page = output.subFolders
32
+ .find((s) => s.name === 'pages')
33
+ ?.files.find((f) => f.name === 'index')
34
+ expect(page?.content).toContain('--swiper-navigation-color: #ff0000')
35
+ expect(page?.content).toContain('--swiper-navigation-size: 24px')
36
+ })
37
+ })
@@ -0,0 +1,58 @@
1
+ import generate from '@babel/generator'
2
+ import { ChunkDefinition, ProjectUIDL } from '@teleporthq/teleport-types'
3
+ import { InlineScriptErrorGuard } from '@teleporthq/teleport-shared'
4
+ import { createDocumentFileChunks } from '../src/utils'
5
+
6
+ /**
7
+ * A script in the project's custom code that throws used to cover every page of
8
+ * the generated app with Next's "Unhandled Runtime Error" overlay. `_document`
9
+ * now runs the inline script error guard before anything else in <head>, and
10
+ * only for a project that has scripts of its own.
11
+ */
12
+
13
+ const uidlWith = (globals: Record<string, unknown>): ProjectUIDL =>
14
+ ({
15
+ name: 'site',
16
+ globals: {
17
+ settings: { title: 'Site', language: 'en' },
18
+ meta: [{ name: 'description', content: 'Site' }],
19
+ assets: [{ type: 'style', path: 'https://fonts.test/font.css' }],
20
+ ...globals,
21
+ },
22
+ root: { name: 'App', node: { type: 'element', content: { elementType: 'Router' } } },
23
+ } as unknown as ProjectUIDL)
24
+
25
+ const documentSource = (uidl: ProjectUIDL): string => {
26
+ const chunks = createDocumentFileChunks(uidl, { assets: {} } as never)
27
+ const chunk = (chunks.js as ChunkDefinition[])[0]
28
+ return generate(chunk.content as never).code
29
+ }
30
+
31
+ describe('generated _document — inline script error guard', () => {
32
+ it('runs before everything else in <head> when the custom code has a script', () => {
33
+ const source = documentSource(
34
+ uidlWith({ customCode: { body: '<script>new Accordion().init()</script>' } })
35
+ )
36
+ const head = source.slice(source.indexOf('<Head>') + '<Head>'.length).trimStart()
37
+
38
+ expect(head.startsWith('<script dangerouslySetInnerHTML={{')).toBe(true)
39
+ expect(source).toContain(JSON.stringify(InlineScriptErrorGuard.INLINE_SCRIPT_ERROR_GUARD))
40
+ expect(source.indexOf('stopImmediatePropagation')).toBeLessThan(
41
+ source.indexOf('new Accordion().init()')
42
+ )
43
+ })
44
+
45
+ it('runs for an inline script asset too', () => {
46
+ const source = documentSource(
47
+ uidlWith({ assets: [{ type: 'script', content: 'console.log(1)' }] })
48
+ )
49
+
50
+ expect(source).toContain('stopImmediatePropagation')
51
+ })
52
+
53
+ it('leaves the document of a project without scripts of its own untouched', () => {
54
+ const source = documentSource(uidlWith({ customCode: { head: '<style>body{}</style>' } }))
55
+
56
+ expect(source).not.toContain('stopImmediatePropagation')
57
+ })
58
+ })
@@ -26,6 +26,9 @@ import { generateQrCodeComponentCode } from '../src/widgets/qrcode-component'
26
26
  import { generateEmojiPickerComponentCode } from '../src/widgets/emoji-picker-component'
27
27
  import { generateColorPickerComponentCode } from '../src/widgets/color-picker-component'
28
28
  import { generateFormFileInputComponentCode } from '../src/widgets/form-file-input-component'
29
+ import { generateScrollSceneComponentCode } from '../src/widgets/scroll-scene-component'
30
+ import { generateScrollVideoComponentCode } from '../src/widgets/scroll-video-component'
31
+ import { generatePageTransitionComponentCode } from '../src/page-transition/page-transition-component'
29
32
  import { COLLAPSIBLE_TEXT_OVERFLOW_COMPONENT_SOURCE } from '../src/collapsible-text/collapsible-text-overflow-component'
30
33
  import { NAV_ACTIVE_LINK_COMPONENT_SOURCE } from '../src/nav-active-link/nav-active-link-component'
31
34
  import { generateRichContentEmbedsComponentCode } from '../src/rich-content-embeds/embed-activator-component'
@@ -39,6 +42,12 @@ const EMITTED_COMPONENTS: Array<[string, string]> = [
39
42
  ['rich-text-editor (with embeds)', generateRichTextEditorComponentCode({ withEmbeds: true })],
40
43
  ['rich-content-embeds', generateRichContentEmbedsComponentCode()],
41
44
  ['tq-motion', generateMotionComponentCode()],
45
+ ['tq-scroll-scene', generateScrollSceneComponentCode()],
46
+ ['tq-scroll-video', generateScrollVideoComponentCode()],
47
+ [
48
+ 'tq-page-transition',
49
+ generatePageTransitionComponentCode({ preset: 'slide-up', duration: 0.35, easing: 'ease-out' }),
50
+ ],
42
51
  ['tq-signature', generateSignatureComponentCode()],
43
52
  ['tq-categories-filter', generateCategoriesFilterComponentCode()],
44
53
  ['tq-categories-megamenu', generateCategoriesMegamenuComponentCode()],
@@ -63,7 +72,7 @@ describe('emitted React components obey the Rules of Hooks', () => {
63
72
 
64
73
  it('covers every component source the generator can emit', () => {
65
74
  // A new widget added without a row here would ship unguarded.
66
- expect(EMITTED_COMPONENTS).toHaveLength(18)
75
+ expect(EMITTED_COMPONENTS).toHaveLength(21)
67
76
  })
68
77
  })
69
78
 
@@ -79,4 +79,74 @@ describe('Next generator with a Motion element', () => {
79
79
  const npmrc = outputFolder.files.find((file) => file.name === '.npmrc')
80
80
  expect(npmrc?.content).toContain('legacy-peer-deps=true')
81
81
  })
82
+
83
+ it('scroll trigger interpolates the FULL from/to state (canvas parity), with scrub + offset support', async () => {
84
+ const outputFolder = await generator.generateProject(buildUidlWithMotion(), template)
85
+ const component = findFile(outputFolder, 'components', 'tq-motion')
86
+ const code = component?.content || ''
87
+
88
+ // The scroll path writes the whole interpolated state (transform families,
89
+ // opacity, filter) to the element — the historical y-only parallax is gone.
90
+ expect(code).toContain('applyScrollState')
91
+ expect(code).toContain('useMotionValueEvent')
92
+ expect(code).not.toContain('parallaxY')
93
+ expect(code).toMatch(/TRANSFORM_KEYS = \['x', 'y', 'scale', 'rotate'\]/)
94
+
95
+ // Scrub smoothing and the scroll-offset windows are wired.
96
+ expect(code).toContain('useSpring')
97
+ expect(code).toContain('SCROLL_OFFSET_RANGES')
98
+ for (const preset of ['pass', 'contained', 'enter', 'exit']) {
99
+ expect(code).toContain(preset)
100
+ }
101
+
102
+ // Blur interpolation mirrors the canvas runtime's BLUR_RE handling.
103
+ expect(code).toContain('blur(')
104
+ })
105
+
106
+ // A site with a page transition wraps every page in an AnimatePresence with
107
+ // initial={false}, which skips the mount animation of every motion component
108
+ // under it. A row that loops by itself (trigger load, repeat -1) never started
109
+ // on the generated site, and stood parked at its end state (2026-10-03).
110
+ it('starts a load animation from a flag set after mount, so a page transition cannot skip it', async () => {
111
+ const outputFolder = await generator.generateProject(buildUidlWithMotion(), template)
112
+ const component = findFile(outputFolder, 'components', 'tq-motion')
113
+ const code = component?.content || ''
114
+
115
+ expect(code).toContain('animate: played ? toVars : fromVars')
116
+ expect(code).toContain(
117
+ 'buildAnimProps(trigger, fromVars, toVars, transition, revealed, played)'
118
+ )
119
+ expect(code).not.toContain(
120
+ "case 'load':\n return { initial: fromVars, animate: toVars, transition }"
121
+ )
122
+ })
123
+
124
+ // Vlad, 2026-10-03: for a visitor who asks for less motion a row that slides
125
+ // by itself stands still and can be swiped, so every item stays reachable.
126
+ it('lets a row that slides by itself be swiped when the visitor asks for less motion', async () => {
127
+ const outputFolder = await generator.generateProject(buildUidlWithMotion(), template)
128
+ const code = findFile(outputFolder, 'components', 'tq-motion')?.content || ''
129
+
130
+ expect(code).toContain('const letMarqueeSwipe = (track) => {')
131
+ expect(code).toContain("frame.style.overflowX = 'auto'")
132
+ expect(code).toContain(
133
+ 'React.useEffect(() => {\n if (still) {\n letMarqueeSwipe(ref.current)'
134
+ )
135
+ })
136
+
137
+ // The server cannot know that a visitor asks for less motion and renders the
138
+ // animated element with its starting style. A first render that drew the still
139
+ // element instead left that style in place: every entrance stayed invisible for
140
+ // those visitors (2026-10-03, React keeps server markup it disagrees with).
141
+ it('draws the still element for less motion only once the page is mounted', async () => {
142
+ const outputFolder = await generator.generateProject(buildUidlWithMotion(), template)
143
+ const code = findFile(outputFolder, 'components', 'tq-motion')?.content || ''
144
+
145
+ expect(code).toContain('React.useEffect(() => setMounted(true), [])')
146
+ expect(code).toContain('const still = mounted && Boolean(shouldReduceMotion)')
147
+ expect(code).toContain(
148
+ 'if (still) {\n return (\n <div ref={ref} style={wrapperStyle} {...rest}>'
149
+ )
150
+ expect(code).not.toContain('if (shouldReduceMotion) {\n return (')
151
+ })
82
152
  })
@@ -223,7 +223,7 @@ describe('generated TqMotion source', () => {
223
223
  expect(code).not.toContain('<motion.div key={index}')
224
224
  // The outer wrapper stays a motion.div so the stagger and group branches render
225
225
  // the same element type (framer's useInView must not lose its node on a flip).
226
- expect(code).toContain('<motion.div ref={ref} style={style} {...rest}>')
226
+ expect(code).toContain('<motion.div ref={ref} style={wrapperStyle} {...rest}>')
227
227
  })
228
228
 
229
229
  it('keeps the framer path for a REPEATING stagger (a CSS transition plays once)', () => {
@@ -237,14 +237,18 @@ describe('generated TqMotion source', () => {
237
237
  describe('generated cssFromState (canvas parity)', () => {
238
238
  const cssFromState = extractCssFromState()
239
239
 
240
- it('collapses the transform family into one string, in the canvas order', () => {
240
+ it("writes the individual transform properties — the author's transform survives (canvas parity)", () => {
241
241
  expect(cssFromState({ opacity: 0, y: 30, scale: 0.8 })).toEqual({
242
242
  opacity: 0,
243
- transform: 'translateY(30px) scale(0.8)',
243
+ translate: '0px 30px',
244
+ scale: '0.8',
244
245
  })
245
246
  expect(cssFromState({ x: -10, y: 4, scale: 2, rotate: -8 })).toEqual({
246
- transform: 'translateX(-10px) translateY(4px) scale(2) rotate(-8deg)',
247
+ translate: '-10px 4px',
248
+ scale: '2',
249
+ rotate: '-8deg',
247
250
  })
251
+ expect(cssFromState({ x: '-25%' })).toEqual({ translate: '-25% 0px' })
248
252
  })
249
253
 
250
254
  it('passes non-transform properties through untouched', () => {