@bendyline/squisq 1.4.1 → 1.5.1
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/README.md +14 -1
- package/dist/{ContentContainer-DNx460y8.d.ts → ContentContainer-BXUlIf18.d.ts} +6 -2
- package/dist/{Doc-BeSeH2iX.d.ts → Doc-BBVGq1_9.d.ts} +922 -273
- package/dist/{ImageEditDoc-BTIvw0oq.d.ts → ImageEditDoc-FuyGtt6o.d.ts} +11 -4
- package/dist/{chunk-ZTX4PFFT.js → chunk-2OIBZYKP.js} +75 -2
- package/dist/chunk-2OIBZYKP.js.map +1 -0
- package/dist/chunk-2UQOHYE2.js +45 -0
- package/dist/chunk-2UQOHYE2.js.map +1 -0
- package/dist/chunk-6ATE2PSM.js +344 -0
- package/dist/chunk-6ATE2PSM.js.map +1 -0
- package/dist/{chunk-57CVQCFX.js → chunk-A4O7GIWE.js} +74 -23
- package/dist/chunk-A4O7GIWE.js.map +1 -0
- package/dist/{chunk-5WFRKURX.js → chunk-DLXZMS5K.js} +4 -2
- package/dist/chunk-DLXZMS5K.js.map +1 -0
- package/dist/chunk-ET53IIEP.js +1 -0
- package/dist/chunk-ET53IIEP.js.map +1 -0
- package/dist/chunk-HWVFJAAH.js +362 -0
- package/dist/chunk-HWVFJAAH.js.map +1 -0
- package/dist/{chunk-2TMAWB4Y.js → chunk-IMSCRLLV.js} +1 -1
- package/dist/chunk-IMSCRLLV.js.map +1 -0
- package/dist/{chunk-B4EEON3N.js → chunk-KKNUBQ6Y.js} +536 -103
- package/dist/chunk-KKNUBQ6Y.js.map +1 -0
- package/dist/chunk-LDQ2HJIX.js +596 -0
- package/dist/chunk-LDQ2HJIX.js.map +1 -0
- package/dist/chunk-LH7I6SH7.js +7208 -0
- package/dist/chunk-LH7I6SH7.js.map +1 -0
- package/dist/chunk-PGGGKYWG.js +1 -0
- package/dist/chunk-PGGGKYWG.js.map +1 -0
- package/dist/{chunk-7UDSRZKG.js → chunk-Q3ROPT5H.js} +35 -14
- package/dist/chunk-Q3ROPT5H.js.map +1 -0
- package/dist/chunk-QE7TWGVT.js +178 -0
- package/dist/chunk-QE7TWGVT.js.map +1 -0
- package/dist/chunk-RUDYOTA6.js +345 -0
- package/dist/chunk-RUDYOTA6.js.map +1 -0
- package/dist/{chunk-K32VJONL.js → chunk-T5UK6YOB.js} +41 -2
- package/dist/chunk-T5UK6YOB.js.map +1 -0
- package/dist/{chunk-3K5OG6KI.js → chunk-TEEEILMP.js} +412 -186
- package/dist/chunk-TEEEILMP.js.map +1 -0
- package/dist/{chunk-FR2RBTKO.js → chunk-TQWLI6S2.js} +93 -27
- package/dist/chunk-TQWLI6S2.js.map +1 -0
- package/dist/{chunk-WMOI7FN3.js → chunk-UY7KGQ5R.js} +12 -2
- package/dist/{chunk-WMOI7FN3.js.map → chunk-UY7KGQ5R.js.map} +1 -1
- package/dist/{chunk-3E5F2XMR.js → chunk-VWUFZ6ZG.js} +29 -2
- package/dist/chunk-VWUFZ6ZG.js.map +1 -0
- package/dist/chunk-ZLQKNOZQ.js +1 -0
- package/dist/chunk-ZLQKNOZQ.js.map +1 -0
- package/dist/doc/index.d.ts +5 -5
- package/dist/doc/index.js +135 -8
- package/dist/generate/index.d.ts +3 -74
- package/dist/generate/index.js +2 -7
- package/dist/icons/index.d.ts +3 -26
- package/dist/icons/index.js +13 -0
- package/dist/icons/inlineIconMarker.d.ts +1 -0
- package/dist/icons/inlineIconMarker.js +15 -0
- package/dist/icons/inlineIconMarker.js.map +1 -0
- package/dist/imageEdit/index.d.ts +12 -6
- package/dist/imageEdit/index.js +4 -1
- package/dist/index.d.ts +14 -11
- package/dist/index.js +265 -37
- package/dist/inlineIconMarker-CiE8Hlvz.d.ts +59 -0
- package/dist/jsonForm/index.d.ts +42 -1
- package/dist/jsonForm/index.js +9 -1
- package/dist/markdown/index.d.ts +228 -3
- package/dist/markdown/index.js +42 -7
- package/dist/recommend/index.d.ts +1 -1
- package/dist/recommend/index.js +3 -2
- package/dist/schemas/index.d.ts +28 -8
- package/dist/schemas/index.js +36 -8
- package/dist/spatial/index.d.ts +9 -4
- package/dist/spatial/index.js +1 -1
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.js +1 -1
- package/dist/story/index.d.ts +1021 -41
- package/dist/story/index.js +135 -8
- package/dist/{themeLibrary-BHjJ0Qx6.d.ts → themeLibrary-CehcJhzz.d.ts} +1 -1
- package/dist/timing/index.js +3 -5
- package/dist/transform/index.d.ts +48 -10
- package/dist/transform/index.js +17 -481
- package/dist/transform/index.js.map +1 -1
- package/dist/{types-BdZkdJ3z.d.ts → types-DlAZ7MW4.d.ts} +115 -1
- package/dist/versions/index.d.ts +1 -1
- package/package.json +6 -1
- package/src/__tests__/animationUtils.test.ts +40 -0
- package/src/__tests__/annotationCoercion.test.ts +243 -0
- package/src/__tests__/applyRenderStyle.test.ts +146 -0
- package/src/__tests__/atmosphereLayers.test.ts +108 -0
- package/src/__tests__/attrTokens.test.ts +162 -0
- package/src/__tests__/contentExtractor.test.ts +60 -0
- package/src/__tests__/coverBlock.test.ts +46 -1
- package/src/__tests__/customTemplateSample.test.ts +144 -0
- package/src/__tests__/customTemplatesFrontmatter.test.ts +236 -0
- package/src/__tests__/customThemesFrontmatter.test.ts +145 -0
- package/src/__tests__/diagramBlock.test.ts +122 -0
- package/src/__tests__/diagramLayout.test.ts +134 -0
- package/src/__tests__/drawing.test.ts +109 -0
- package/src/__tests__/drawingBlock.test.ts +124 -0
- package/src/__tests__/drawingLayout.test.ts +171 -0
- package/src/__tests__/geohash.test.ts +6 -5
- package/src/__tests__/getLayers.test.ts +6 -2
- package/src/__tests__/getLayersFallback.test.ts +103 -0
- package/src/__tests__/imageEditExportSvg.test.ts +70 -0
- package/src/__tests__/imageEditVersions.test.ts +72 -0
- package/src/__tests__/inlineIconMarker.test.ts +36 -0
- package/src/__tests__/inputDescriptors.test.ts +162 -0
- package/src/__tests__/jsonForm.tokens.test.ts +84 -0
- package/src/__tests__/layoutBlock.test.ts +83 -0
- package/src/__tests__/layoutLayout.test.ts +169 -0
- package/src/__tests__/markdown.test.ts +87 -0
- package/src/__tests__/markdownSanitize.test.ts +79 -0
- package/src/__tests__/markdownToDoc.test.ts +391 -2
- package/src/__tests__/mediaAnnotations.test.ts +91 -0
- package/src/__tests__/mediaSchedule.test.ts +118 -0
- package/src/__tests__/pandocAttrParse.test.ts +187 -0
- package/src/__tests__/resolveDocTheme.test.ts +55 -0
- package/src/__tests__/rootExports.test.ts +43 -0
- package/src/__tests__/shapeGeometry.test.ts +89 -0
- package/src/__tests__/shapeVocabulary.test.ts +133 -0
- package/src/__tests__/structuredData.test.ts +249 -0
- package/src/__tests__/templateAnnotationParse.test.ts +108 -0
- package/src/__tests__/templateMetadata.test.ts +31 -0
- package/src/__tests__/templates.test.ts +257 -4
- package/src/__tests__/themeCompile.test.ts +42 -0
- package/src/__tests__/themeValidator.test.ts +6 -0
- package/src/__tests__/transformV2.test.ts +137 -0
- package/src/__tests__/transitionNormalize.test.ts +61 -0
- package/src/__tests__/validateDoc.test.ts +260 -0
- package/src/doc/annotationBlocks.ts +117 -0
- package/src/doc/customTemplatesFrontmatter.ts +314 -0
- package/src/doc/customThemesFrontmatter.ts +95 -0
- package/src/doc/docToMarkdown.ts +104 -5
- package/src/doc/getLayers.ts +49 -17
- package/src/doc/index.ts +47 -1
- package/src/doc/markdownToDoc.ts +411 -13
- package/src/doc/mediaAnnotations.ts +100 -0
- package/src/doc/resolveDocTheme.ts +55 -0
- package/src/doc/standaloneAnnotation.ts +58 -0
- package/src/doc/structuredData.ts +301 -0
- package/src/doc/templateInputs.ts +240 -0
- package/src/doc/templates/__tests__/customTemplate.test.ts +170 -0
- package/src/doc/templates/__tests__/customTemplateRegistry.test.ts +110 -0
- package/src/doc/templates/accentImage.ts +30 -6
- package/src/doc/templates/captionUtils.ts +23 -0
- package/src/doc/templates/comparisonBar.ts +33 -18
- package/src/doc/templates/coverBlock.ts +35 -5
- package/src/doc/templates/customTemplate.ts +51 -0
- package/src/doc/templates/dataTable.ts +37 -19
- package/src/doc/templates/dateEvent.ts +41 -20
- package/src/doc/templates/definitionCard.ts +26 -18
- package/src/doc/templates/diagramBlock.ts +204 -0
- package/src/doc/templates/diagramLayout.ts +159 -0
- package/src/doc/templates/drawingBlock.ts +325 -0
- package/src/doc/templates/drawingLayout.ts +539 -0
- package/src/doc/templates/factCard.ts +54 -26
- package/src/doc/templates/fallbackBlock.ts +123 -0
- package/src/doc/templates/featureBlock.ts +20 -13
- package/src/doc/templates/fullBleedQuote.ts +18 -8
- package/src/doc/templates/imageWithCaption.ts +30 -15
- package/src/doc/templates/index.ts +225 -46
- package/src/doc/templates/inputDescriptors.ts +432 -0
- package/src/doc/templates/layoutBlock.ts +43 -0
- package/src/doc/templates/layoutLayout.ts +243 -0
- package/src/doc/templates/listBlock.ts +56 -30
- package/src/doc/templates/mapBlock.ts +15 -12
- package/src/doc/templates/metadata.ts +120 -0
- package/src/doc/templates/persistentLayers.ts +129 -11
- package/src/doc/templates/photoGrid.ts +22 -34
- package/src/doc/templates/pullQuote.ts +30 -12
- package/src/doc/templates/quoteBlock.ts +54 -29
- package/src/doc/templates/sectionHeader.ts +25 -11
- package/src/doc/templates/statHighlight.ts +39 -22
- package/src/doc/templates/titleBlock.ts +59 -21
- package/src/doc/templates/tokens/__tests__/resolveTokens.test.ts +487 -0
- package/src/doc/templates/tokens/resolveTokens.ts +453 -0
- package/src/doc/templates/twoColumn.ts +48 -30
- package/src/doc/templates/videoPullQuote.ts +22 -12
- package/src/doc/templates/videoWithCaption.ts +21 -15
- package/src/doc/utils/animationUtils.ts +267 -3
- package/src/doc/utils/applyRenderStyle.ts +113 -0
- package/src/doc/utils/imageTreatment.ts +70 -0
- package/src/doc/utils/nearest.ts +59 -0
- package/src/doc/utils/shapeGeometry.ts +464 -0
- package/src/doc/utils/themeUtils.ts +75 -1
- package/src/doc/validate.ts +575 -0
- package/src/generate/contentExtractor.ts +148 -35
- package/src/generate/index.ts +0 -3
- package/src/icons/index.ts +8 -0
- package/src/icons/inlineIconMarker.ts +73 -0
- package/src/imageEdit/export.ts +85 -2
- package/src/imageEdit/index.ts +1 -0
- package/src/imageEdit/persistence.ts +51 -0
- package/src/index.ts +2 -0
- package/src/jsonForm/index.ts +2 -0
- package/src/jsonForm/tokens.ts +78 -0
- package/src/markdown/annotationCoercion.ts +295 -0
- package/src/markdown/attrTokens.ts +241 -0
- package/src/markdown/convert.ts +229 -26
- package/src/markdown/index.ts +37 -0
- package/src/markdown/sanitize.ts +318 -0
- package/src/markdown/stringify.ts +96 -6
- package/src/markdown/types.ts +66 -0
- package/src/markdown/utils.ts +111 -2
- package/src/recommend/templates.ts +24 -0
- package/src/schemas/BlockTemplates.ts +133 -4
- package/src/schemas/CustomTemplates.ts +126 -0
- package/src/schemas/Doc.ts +387 -65
- package/src/schemas/ImageEditDoc.ts +11 -4
- package/src/schemas/Media.ts +169 -0
- package/src/schemas/Theme.ts +23 -1
- package/src/schemas/Transitions.ts +224 -0
- package/src/schemas/colorUtils.ts +33 -0
- package/src/schemas/fontStacks.ts +5 -2
- package/src/schemas/index.ts +3 -0
- package/src/schemas/themeCompile.ts +24 -5
- package/src/schemas/themeValidator.ts +49 -11
- package/src/schemas/themes/cinematic.json +69 -12
- package/src/schemas/themes/documentary.json +58 -11
- package/src/schemas/themes/gezellig.json +60 -11
- package/src/schemas/themes/magazine.json +54 -9
- package/src/schemas/themes/morning-light.json +52 -9
- package/src/schemas/themes/standard-dark.json +48 -13
- package/src/schemas/themes/standard.json +44 -12
- package/src/schemas/themes/tech-dark.json +65 -12
- package/src/spatial/Geohash.ts +52 -21
- package/src/storage/ContentContainer.ts +6 -2
- package/src/transform/applyTransform.ts +43 -4
- package/src/transform/index.ts +1 -0
- package/src/transform/registry.ts +44 -13
- package/src/transform/styles/dataDriven.ts +2 -1
- package/src/transform/styles/documentary.ts +2 -1
- package/src/transform/styles/magazine.ts +3 -1
- package/src/transform/styles/minimal.ts +2 -1
- package/src/transform/styles/narrative.ts +3 -1
- package/src/transform/templateSelector.ts +87 -11
- package/src/transform/types.ts +35 -2
- package/dist/chunk-2TMAWB4Y.js.map +0 -1
- package/dist/chunk-3E5F2XMR.js.map +0 -1
- package/dist/chunk-3K5OG6KI.js.map +0 -1
- package/dist/chunk-4X3JQXNM.js +0 -35
- package/dist/chunk-4X3JQXNM.js.map +0 -1
- package/dist/chunk-57CVQCFX.js.map +0 -1
- package/dist/chunk-5WFRKURX.js.map +0 -1
- package/dist/chunk-7UDSRZKG.js.map +0 -1
- package/dist/chunk-B4EEON3N.js.map +0 -1
- package/dist/chunk-FR2RBTKO.js.map +0 -1
- package/dist/chunk-H3AXU4MJ.js +0 -179
- package/dist/chunk-H3AXU4MJ.js.map +0 -1
- package/dist/chunk-K32VJONL.js.map +0 -1
- package/dist/chunk-KSWJR66U.js +0 -3706
- package/dist/chunk-KSWJR66U.js.map +0 -1
- package/dist/chunk-MYIH7FWD.js +0 -145
- package/dist/chunk-MYIH7FWD.js.map +0 -1
- package/dist/chunk-ZTX4PFFT.js.map +0 -1
- package/src/__tests__/slideshowGenerator.test.ts +0 -128
- package/src/generate/slideshowGenerator.ts +0 -273
|
@@ -10,6 +10,10 @@ import {
|
|
|
10
10
|
VIEWPORT_PRESETS,
|
|
11
11
|
} from '../doc/templates/index';
|
|
12
12
|
import type { TemplateBlock } from '../schemas/BlockTemplates';
|
|
13
|
+
import type { TextLayer } from '../schemas/Doc';
|
|
14
|
+
import { getLayers } from '../doc/getLayers';
|
|
15
|
+
import { markdownToDoc } from '../doc/markdownToDoc';
|
|
16
|
+
import { parseMarkdown } from '../markdown/parse';
|
|
13
17
|
|
|
14
18
|
describe('templateRegistry', () => {
|
|
15
19
|
it('contains all expected templates', () => {
|
|
@@ -137,7 +141,7 @@ describe('expandTemplateBlock', () => {
|
|
|
137
141
|
expect(imageLayer!.position.height).toBe('90%');
|
|
138
142
|
});
|
|
139
143
|
|
|
140
|
-
it('expands rightFeature template with image on the right half and
|
|
144
|
+
it('expands rightFeature template with image on the right half and left-aligned text on the left', () => {
|
|
141
145
|
const block: TemplateBlock = {
|
|
142
146
|
template: 'rightFeature',
|
|
143
147
|
id: 'rf-1',
|
|
@@ -157,9 +161,11 @@ describe('expandTemplateBlock', () => {
|
|
|
157
161
|
expect(imageLayer!.position.width).toBe('50%');
|
|
158
162
|
const textLayers = layers.filter((l) => l.type === 'text');
|
|
159
163
|
for (const t of textLayers) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
expect(
|
|
164
|
+
// The mirror lives in the layout only: the text column occupies the
|
|
165
|
+
// left half, but the running text stays left-aligned for readability.
|
|
166
|
+
expect(t.position.x).toBe('6%');
|
|
167
|
+
expect(t.position.anchor).toBe('top-left');
|
|
168
|
+
expect((t.content as { style: { textAlign?: string } }).style.textAlign).toBe('left');
|
|
163
169
|
}
|
|
164
170
|
});
|
|
165
171
|
|
|
@@ -242,6 +248,75 @@ describe('expandTemplateBlock', () => {
|
|
|
242
248
|
expect((result.layers ?? []).length).toBeGreaterThanOrEqual(1);
|
|
243
249
|
}
|
|
244
250
|
});
|
|
251
|
+
|
|
252
|
+
it('keeps photoGrid media captions compact without placeholder alt labels', () => {
|
|
253
|
+
const block: TemplateBlock = {
|
|
254
|
+
template: 'photoGrid',
|
|
255
|
+
id: 'photo-grid-captions',
|
|
256
|
+
duration: 10,
|
|
257
|
+
audioSegment: 0,
|
|
258
|
+
images: [
|
|
259
|
+
{ src: 'one.jpg', alt: 'A long archival caption for the first image' },
|
|
260
|
+
{ src: 'two.jpg', alt: 'A long archival caption for the second image' },
|
|
261
|
+
{ src: 'three.jpg', alt: 'A long archival caption for the third image' },
|
|
262
|
+
],
|
|
263
|
+
caption:
|
|
264
|
+
'A long Wikimedia description with source notes, place names, dates, and details that should remain compact.',
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const context = createTemplateContext(DEFAULT_THEME, 0, 10, VIEWPORT_PRESETS.landscape);
|
|
268
|
+
const result = expandTemplateBlock(block, context);
|
|
269
|
+
const layers = result.layers ?? [];
|
|
270
|
+
const captionLayer = layers.find(
|
|
271
|
+
(layer): layer is TextLayer => layer.type === 'text' && layer.id === 'caption',
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
expect(layers.some((layer) => layer.id.startsWith('grid-alt-'))).toBe(false);
|
|
275
|
+
expect(captionLayer).toBeDefined();
|
|
276
|
+
expect(captionLayer!.content.style.maxLines).toBe(1);
|
|
277
|
+
expect(captionLayer!.position.width).toBe('78%');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('clamps full-bleed media captions above playback controls', () => {
|
|
281
|
+
const imageBlock: TemplateBlock = {
|
|
282
|
+
template: 'imageWithCaption',
|
|
283
|
+
id: 'image-caption',
|
|
284
|
+
duration: 10,
|
|
285
|
+
audioSegment: 0,
|
|
286
|
+
imageSrc: 'image.jpg',
|
|
287
|
+
imageAlt: 'Image',
|
|
288
|
+
caption:
|
|
289
|
+
'A long Wikimedia description with source notes, place names, dates, and details that should remain compact.',
|
|
290
|
+
};
|
|
291
|
+
const videoBlock: TemplateBlock = {
|
|
292
|
+
template: 'videoWithCaption',
|
|
293
|
+
id: 'video-caption',
|
|
294
|
+
duration: 10,
|
|
295
|
+
audioSegment: 0,
|
|
296
|
+
videoSrc: 'video.mp4',
|
|
297
|
+
videoAlt: 'Video',
|
|
298
|
+
clipStart: 0,
|
|
299
|
+
clipEnd: 10,
|
|
300
|
+
caption: imageBlock.caption,
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const context = createTemplateContext(DEFAULT_THEME, 0, 10, VIEWPORT_PRESETS.landscape);
|
|
304
|
+
const imageCaption = (expandTemplateBlock(imageBlock, context).layers ?? []).find(
|
|
305
|
+
(layer): layer is TextLayer => layer.type === 'text' && layer.id === 'caption',
|
|
306
|
+
);
|
|
307
|
+
const videoCaption = (expandTemplateBlock(videoBlock, context).layers ?? []).find(
|
|
308
|
+
(layer): layer is TextLayer => layer.type === 'text' && layer.id === 'caption',
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
expect(imageCaption).toBeDefined();
|
|
312
|
+
expect(videoCaption).toBeDefined();
|
|
313
|
+
expect(imageCaption!.content.style.maxLines).toBe(2);
|
|
314
|
+
expect(videoCaption!.content.style.maxLines).toBe(2);
|
|
315
|
+
expect(imageCaption!.position.y).toBe('74%');
|
|
316
|
+
expect(videoCaption!.position.y).toBe('74%');
|
|
317
|
+
expect(imageCaption!.position.width).toBe('78%');
|
|
318
|
+
expect(videoCaption!.position.width).toBe('78%');
|
|
319
|
+
});
|
|
245
320
|
});
|
|
246
321
|
|
|
247
322
|
describe('expandDocBlocks', () => {
|
|
@@ -309,6 +384,145 @@ describe('expandDocBlocks', () => {
|
|
|
309
384
|
expect((landscape[0].layers ?? []).length).toBeGreaterThan(0);
|
|
310
385
|
expect((portrait[0].layers ?? []).length).toBeGreaterThan(0);
|
|
311
386
|
});
|
|
387
|
+
|
|
388
|
+
describe('theme renderStyle wiring', () => {
|
|
389
|
+
const blocks = (): TemplateBlock[] => [
|
|
390
|
+
{ template: 'title', id: 't-1', duration: 5, audioSegment: 0, title: 'One' },
|
|
391
|
+
{
|
|
392
|
+
template: 'imageWithCaption',
|
|
393
|
+
id: 't-2',
|
|
394
|
+
duration: 6,
|
|
395
|
+
audioSegment: 0,
|
|
396
|
+
imageSrc: 'photo.jpg',
|
|
397
|
+
imageAlt: 'photo',
|
|
398
|
+
caption: 'A caption',
|
|
399
|
+
},
|
|
400
|
+
];
|
|
401
|
+
|
|
402
|
+
it('fills the theme default transition on blocks after the first, never block 0', () => {
|
|
403
|
+
const theme = {
|
|
404
|
+
...DEFAULT_THEME,
|
|
405
|
+
renderStyle: {
|
|
406
|
+
...DEFAULT_THEME.renderStyle,
|
|
407
|
+
defaultTransition: { type: 'dissolve' as const, duration: 1.1 },
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
const result = expandDocBlocks(blocks(), { theme });
|
|
411
|
+
expect(result[0].transition).toBeUndefined();
|
|
412
|
+
expect(result[1].transition).toEqual({ type: 'dissolve', duration: 1.1 });
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('never overrides an authored block transition', () => {
|
|
416
|
+
const theme = {
|
|
417
|
+
...DEFAULT_THEME,
|
|
418
|
+
renderStyle: {
|
|
419
|
+
...DEFAULT_THEME.renderStyle,
|
|
420
|
+
defaultTransition: { type: 'dissolve' as const },
|
|
421
|
+
},
|
|
422
|
+
};
|
|
423
|
+
const authored = blocks();
|
|
424
|
+
authored[1].transition = { type: 'cut' };
|
|
425
|
+
const result = expandDocBlocks(authored, { theme });
|
|
426
|
+
expect(result[1].transition).toEqual({ type: 'cut' });
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('scales template animation durations by theme animationSpeed', () => {
|
|
430
|
+
const theme = {
|
|
431
|
+
...DEFAULT_THEME,
|
|
432
|
+
style: { ...DEFAULT_THEME.style, animationSpeed: 2.0 },
|
|
433
|
+
};
|
|
434
|
+
const base = expandDocBlocks(blocks());
|
|
435
|
+
const slowed = expandDocBlocks(blocks(), { theme });
|
|
436
|
+
const baseTitle = base[0].layers?.find((l) => l.id === 'title');
|
|
437
|
+
const slowedTitle = slowed[0].layers?.find((l) => l.id === 'title');
|
|
438
|
+
expect(slowedTitle?.animation?.duration).toBe((baseTitle?.animation?.duration ?? 0) * 2);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('gives full-bleed imagery ambient Ken Burns when the theme opts in', () => {
|
|
442
|
+
const theme = {
|
|
443
|
+
...DEFAULT_THEME,
|
|
444
|
+
renderStyle: { ...DEFAULT_THEME.renderStyle, ambientMotion: true },
|
|
445
|
+
};
|
|
446
|
+
const result = expandDocBlocks(blocks(), { theme });
|
|
447
|
+
const bg = result[1].layers?.find((l) => l.id === 'bg-image');
|
|
448
|
+
expect(bg?.animation?.type).toBe('slowZoom');
|
|
449
|
+
// Standard theme (ambientMotion: false) leaves the image static.
|
|
450
|
+
const plain = expandDocBlocks(blocks());
|
|
451
|
+
const plainBg = plain[1].layers?.find((l) => l.id === 'bg-image');
|
|
452
|
+
expect(plainBg?.animation).toBeUndefined();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('themedEntrance: theme default text animation overrides entrance type, keeps timing', () => {
|
|
456
|
+
const theme = {
|
|
457
|
+
...DEFAULT_THEME,
|
|
458
|
+
renderStyle: { ...DEFAULT_THEME.renderStyle, defaultTextAnimation: 'zoomIn' as const },
|
|
459
|
+
};
|
|
460
|
+
const result = expandDocBlocks(blocks(), { theme });
|
|
461
|
+
const title = result[0].layers?.find((l) => l.id === 'title');
|
|
462
|
+
expect(title?.animation?.type).toBe('zoomIn');
|
|
463
|
+
// Duration authored by the template is preserved (title uses 2s)
|
|
464
|
+
expect(title?.animation?.duration).toBe(2);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('templateHints: dramatic entrance switches statHighlight timing', () => {
|
|
468
|
+
const statBlock: TemplateBlock[] = [
|
|
469
|
+
{
|
|
470
|
+
template: 'statHighlight',
|
|
471
|
+
id: 's-1',
|
|
472
|
+
duration: 5,
|
|
473
|
+
audioSegment: 0,
|
|
474
|
+
stat: '42%',
|
|
475
|
+
description: 'described',
|
|
476
|
+
},
|
|
477
|
+
];
|
|
478
|
+
const theme = {
|
|
479
|
+
...DEFAULT_THEME,
|
|
480
|
+
renderStyle: {
|
|
481
|
+
...DEFAULT_THEME.renderStyle,
|
|
482
|
+
templateHints: { statHighlight: { entrance: 'dramatic' } },
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
const hinted = expandDocBlocks(statBlock, { theme });
|
|
486
|
+
const plain = expandDocBlocks(statBlock);
|
|
487
|
+
const hintedStat = hinted[0].layers?.find((l) => l.id === 'stat');
|
|
488
|
+
const plainStat = plain[0].layers?.find((l) => l.id === 'stat');
|
|
489
|
+
expect(hintedStat?.animation?.duration).toBe(0.4);
|
|
490
|
+
expect(plainStat?.animation?.duration).toBe(0.6);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('theme persistentLayers render for docs without their own (wholesale precedence)', () => {
|
|
494
|
+
const theme = {
|
|
495
|
+
...DEFAULT_THEME,
|
|
496
|
+
persistentLayers: {
|
|
497
|
+
bottomLayers: [
|
|
498
|
+
{
|
|
499
|
+
template: 'solidBackground' as const,
|
|
500
|
+
config: { type: 'solidBackground' as const, color: '#112233' },
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
const inherited = expandDocBlocks(blocks(), { theme });
|
|
506
|
+
expect(inherited[0].layers?.[0]?.id).toContain('solid');
|
|
507
|
+
|
|
508
|
+
// Doc's own persistent layers win wholesale
|
|
509
|
+
const docOwn = expandDocBlocks(blocks(), {
|
|
510
|
+
theme,
|
|
511
|
+
persistentLayers: {
|
|
512
|
+
bottomLayers: [
|
|
513
|
+
{
|
|
514
|
+
template: 'solidBackground' as const,
|
|
515
|
+
config: { type: 'solidBackground' as const, color: '#445566' },
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
const first = docOwn[0].layers?.[0];
|
|
521
|
+
expect(first && 'content' in first && (first.content as { fill?: string }).fill).toBe(
|
|
522
|
+
'#445566',
|
|
523
|
+
);
|
|
524
|
+
});
|
|
525
|
+
});
|
|
312
526
|
});
|
|
313
527
|
|
|
314
528
|
describe('DEFAULT_THEME', () => {
|
|
@@ -319,6 +533,45 @@ describe('DEFAULT_THEME', () => {
|
|
|
319
533
|
});
|
|
320
534
|
});
|
|
321
535
|
|
|
536
|
+
describe('inline-param coercion → getLayers', () => {
|
|
537
|
+
function firstBlock(md: string) {
|
|
538
|
+
return markdownToDoc(parseMarkdown(md), { generateCoverBlock: false }).blocks[0];
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
it('renders a map from pure-inline {[map center=… zoom=…]} annotation', () => {
|
|
542
|
+
const block = firstBlock('# Downtown {[map center="47.6,-122.3" zoom=9 mapStyle="road"]}\n');
|
|
543
|
+
// Raw overrides stay strings for lossless round-trip.
|
|
544
|
+
expect(block.templateOverrides).toEqual({
|
|
545
|
+
center: '47.6,-122.3',
|
|
546
|
+
zoom: '9',
|
|
547
|
+
mapStyle: 'road',
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
const layers = getLayers(block, {});
|
|
551
|
+
const mapLayer = layers.find((l) => l.type === 'map');
|
|
552
|
+
expect(mapLayer).toBeDefined();
|
|
553
|
+
// Coerced center/zoom flow into the map layer's content — not empty strings.
|
|
554
|
+
const content = mapLayer!.content as { center: { lat: number; lng: number }; zoom: number };
|
|
555
|
+
expect(content.center).toEqual({ lat: 47.6, lng: -122.3 });
|
|
556
|
+
expect(content.zoom).toBe(9);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('renders a twoColumn from pure-inline labeled-pair annotation', () => {
|
|
560
|
+
const block = firstBlock('# Coffee {[twoColumn left="Espresso|Bold" right="Filter|Smooth"]}\n');
|
|
561
|
+
const layers = getLayers(block, {});
|
|
562
|
+
// Guard requires both labels — a non-empty layer set proves coercion worked.
|
|
563
|
+
expect(layers.length).toBeGreaterThan(0);
|
|
564
|
+
const texts = layers
|
|
565
|
+
.filter((l) => l.type === 'text')
|
|
566
|
+
.map((l) => (l.content as { text: string }).text);
|
|
567
|
+
expect(texts).toContain('Espresso');
|
|
568
|
+
expect(texts).toContain('Filter');
|
|
569
|
+
// The sublabels from the "|" split render too.
|
|
570
|
+
expect(texts).toContain('Bold');
|
|
571
|
+
expect(texts).toContain('Smooth');
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
322
575
|
describe('VIEWPORT_PRESETS', () => {
|
|
323
576
|
it('has landscape and portrait presets', () => {
|
|
324
577
|
expect(VIEWPORT_PRESETS).toHaveProperty('landscape');
|
|
@@ -71,6 +71,48 @@ describe('compileTheme', () => {
|
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
describe('compileTheme base inheritance', () => {
|
|
75
|
+
it('inherits renderStyle and colorSchemes from a chosen base', () => {
|
|
76
|
+
const base = resolveTheme('cinematic');
|
|
77
|
+
const theme = compileTheme(
|
|
78
|
+
{ id: 'brand', name: 'Brand', seedColors: { primary: '#3182ce' } },
|
|
79
|
+
{ base },
|
|
80
|
+
);
|
|
81
|
+
expect(theme.basedOn).toBe('cinematic');
|
|
82
|
+
// renderStyle inherited from the base (not the starter's 'standard')
|
|
83
|
+
expect(theme.renderStyle.name).toBe(base.renderStyle.name);
|
|
84
|
+
// colorSchemes inherited wholesale from the base
|
|
85
|
+
expect(theme.colorSchemes).toEqual(base.colorSchemes);
|
|
86
|
+
// ...but colors are still derived from the user's seed
|
|
87
|
+
expect(theme.colors.primary).toBe('#3182ce');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('replaces colorSchemes wholesale when the partial supplies them (removal sticks)', () => {
|
|
91
|
+
const base = resolveTheme('cinematic');
|
|
92
|
+
const theme = compileTheme(
|
|
93
|
+
{
|
|
94
|
+
id: 'brand',
|
|
95
|
+
name: 'Brand',
|
|
96
|
+
seedColors: { primary: '#3182ce' },
|
|
97
|
+
colorSchemes: { onlyOne: { bg: '#111111', text: '#eeeeee', accent: '#3182ce' } },
|
|
98
|
+
},
|
|
99
|
+
{ base },
|
|
100
|
+
);
|
|
101
|
+
// Only the scheme the user kept — the base's blue/green/… are gone.
|
|
102
|
+
expect(Object.keys(theme.colorSchemes)).toEqual(['onlyOne']);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('defaults to the neutral starter when no base is given (basedOn unset)', () => {
|
|
106
|
+
const theme = compileTheme({
|
|
107
|
+
id: 'brand',
|
|
108
|
+
name: 'Brand',
|
|
109
|
+
seedColors: { primary: '#3182ce' },
|
|
110
|
+
});
|
|
111
|
+
expect(theme.basedOn).toBeUndefined();
|
|
112
|
+
expect(theme.renderStyle.name).toBe('standard');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
74
116
|
describe('parseTheme / serializeTheme round-trip', () => {
|
|
75
117
|
it('serialize → parse is lossless for built-ins', () => {
|
|
76
118
|
for (const [id, theme] of Object.entries(THEMES)) {
|
|
@@ -105,6 +105,12 @@ describe('validateTheme', () => {
|
|
|
105
105
|
expect(validateTheme(t).valid).toBe(true);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
it('accepts PowerPoint-style default transitions', () => {
|
|
109
|
+
const t = clone(DEFAULT_THEME);
|
|
110
|
+
t.renderStyle.defaultTransition = { type: 'checkerboard', duration: 1 };
|
|
111
|
+
expect(validateTheme(t).valid).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
108
114
|
it('rejects seedColors without primary', () => {
|
|
109
115
|
const bad = clone(DEFAULT_THEME);
|
|
110
116
|
bad.seedColors = { accent: '#63b3ed' } as never;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
applyTransform,
|
|
4
|
+
resolveTransformStyle,
|
|
5
|
+
registerTransformStyle,
|
|
6
|
+
unregisterTransformStyle,
|
|
7
|
+
getTransformStyleIds,
|
|
8
|
+
} from '../transform/index.js';
|
|
9
|
+
import type { TransformStyleConfig } from '../transform/index.js';
|
|
10
|
+
import type { Doc, Block } from '../schemas/Doc.js';
|
|
11
|
+
import { isTemplateBlock } from '../schemas/BlockTemplates.js';
|
|
12
|
+
|
|
13
|
+
function paragraphBlock(id: string, title: string, text: string, duration = 20): Block {
|
|
14
|
+
return {
|
|
15
|
+
id,
|
|
16
|
+
startTime: 0,
|
|
17
|
+
duration,
|
|
18
|
+
audioSegment: 0,
|
|
19
|
+
title,
|
|
20
|
+
contents: [
|
|
21
|
+
{
|
|
22
|
+
type: 'paragraph',
|
|
23
|
+
children: [{ type: 'text', value: text }],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Body with a strong quote extraction. */
|
|
30
|
+
const QUOTED =
|
|
31
|
+
'The ranger paused at the overlook. "The mountain decides who climbs it," she said quietly. ' +
|
|
32
|
+
'Every year thousands of visitors make the same journey up the winding road to Paradise.';
|
|
33
|
+
|
|
34
|
+
function sampleDoc(): Doc {
|
|
35
|
+
return {
|
|
36
|
+
articleId: 'transform-v2-test',
|
|
37
|
+
duration: 60,
|
|
38
|
+
blocks: [
|
|
39
|
+
paragraphBlock('b1', 'The Mountain', QUOTED),
|
|
40
|
+
paragraphBlock(
|
|
41
|
+
'b2',
|
|
42
|
+
'Visitors',
|
|
43
|
+
'In 2024 the park recorded 2.5 million visitors, a 40% increase over the prior decade. ' +
|
|
44
|
+
'Rangers attribute the growth to social media and improved road access.',
|
|
45
|
+
),
|
|
46
|
+
],
|
|
47
|
+
audio: { segments: [] },
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('transform style contract v2', () => {
|
|
52
|
+
afterEach(() => unregisterTransformStyle('custom-test'));
|
|
53
|
+
|
|
54
|
+
it('suggestedThemeId applies only when the doc declares no theme', () => {
|
|
55
|
+
const { doc } = applyTransform(sampleDoc(), 'magazine');
|
|
56
|
+
expect(doc.themeId).toBe('magazine');
|
|
57
|
+
|
|
58
|
+
const themed = { ...sampleDoc(), themeId: 'gezellig' };
|
|
59
|
+
expect(applyTransform(themed, 'magazine').doc.themeId).toBe('gezellig');
|
|
60
|
+
|
|
61
|
+
const viaOption = applyTransform(sampleDoc(), 'magazine', { themeId: 'bold' });
|
|
62
|
+
expect(viaOption.doc.themeId).toBe('bold');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('templateMap remaps quote extractions (magazine → pullQuote with accent image as background)', () => {
|
|
66
|
+
const images = [{ src: 'photo.jpg', alt: 'a photo' }];
|
|
67
|
+
const { doc } = applyTransform(sampleDoc(), 'magazine', { images });
|
|
68
|
+
const templates = doc.blocks.filter(isTemplateBlock).map((b) => b.template);
|
|
69
|
+
// The quote extraction maps to pullQuote (magazine's templateMap)
|
|
70
|
+
// because an image was available to serve as the background.
|
|
71
|
+
expect(templates).toContain('pullQuote');
|
|
72
|
+
expect(templates).not.toContain('quote');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('templateMap remap falls back to the default when the target needs an unavailable image', () => {
|
|
76
|
+
const { doc } = applyTransform(sampleDoc(), 'magazine', { images: [] });
|
|
77
|
+
const templates = doc.blocks.filter(isTemplateBlock).map((b) => b.template);
|
|
78
|
+
expect(templates).toContain('quote');
|
|
79
|
+
expect(templates).not.toContain('pullQuote');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('pacing adds intro/outro bookends (narrative)', () => {
|
|
83
|
+
const { doc } = applyTransform(sampleDoc(), 'narrative');
|
|
84
|
+
const first = doc.blocks[0];
|
|
85
|
+
const last = doc.blocks[doc.blocks.length - 1];
|
|
86
|
+
expect(first.template).toBe('title');
|
|
87
|
+
expect(first.title ?? (first as { title?: string }).title).toBeDefined();
|
|
88
|
+
expect(last.template).toBe('sectionHeader');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('registerTransformStyle adds a resolvable style that wins over unknown fallback', () => {
|
|
92
|
+
const custom: TransformStyleConfig = {
|
|
93
|
+
...resolveTransformStyle('minimal'),
|
|
94
|
+
id: 'custom-test',
|
|
95
|
+
name: 'Custom Test',
|
|
96
|
+
description: 'test style',
|
|
97
|
+
};
|
|
98
|
+
registerTransformStyle(custom);
|
|
99
|
+
expect(getTransformStyleIds()).toContain('custom-test');
|
|
100
|
+
expect(resolveTransformStyle('custom-test').name).toBe('Custom Test');
|
|
101
|
+
unregisterTransformStyle('custom-test');
|
|
102
|
+
// Unknown id falls back to the default (documentary)
|
|
103
|
+
expect(resolveTransformStyle('custom-test').id).toBe('documentary');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("accepts 'dataDriven' as an alias for the hyphenated registry id", () => {
|
|
107
|
+
expect(resolveTransformStyle('dataDriven').id).toBe('data-driven');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('budget.slidesPerMinute caps promotions on long docs', () => {
|
|
111
|
+
const manyBlocks: Doc = {
|
|
112
|
+
articleId: 'budget-test',
|
|
113
|
+
duration: 60,
|
|
114
|
+
blocks: Array.from({ length: 12 }, (_, i) =>
|
|
115
|
+
paragraphBlock(
|
|
116
|
+
`b${i}`,
|
|
117
|
+
`Section ${i}`,
|
|
118
|
+
`In 2024 the park recorded ${(i + 2) * 7}% growth across all visitor categories nationwide.`,
|
|
119
|
+
5,
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
audio: { segments: [] },
|
|
123
|
+
};
|
|
124
|
+
const unbudgeted = resolveTransformStyle('data-driven');
|
|
125
|
+
const budgeted: TransformStyleConfig = {
|
|
126
|
+
...unbudgeted,
|
|
127
|
+
id: 'custom-test',
|
|
128
|
+
budget: { slidesPerMinute: 2 },
|
|
129
|
+
};
|
|
130
|
+
registerTransformStyle(budgeted);
|
|
131
|
+
const loose = applyTransform(manyBlocks, 'data-driven');
|
|
132
|
+
const tight = applyTransform(manyBlocks, 'custom-test');
|
|
133
|
+
expect(tight.stats.transformedBlocks).toBeLessThan(loose.stats.transformedBlocks);
|
|
134
|
+
// 12 blocks x 5s = 60s → 2/minute → at most 2 promotions
|
|
135
|
+
expect(tight.stats.transformedBlocks).toBeLessThanOrEqual(2);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
normalizeTransitionType,
|
|
4
|
+
normalizeTransitionDirection,
|
|
5
|
+
isTransitionType,
|
|
6
|
+
TRANSITION_TYPES,
|
|
7
|
+
} from '../schemas/Transitions';
|
|
8
|
+
|
|
9
|
+
describe('normalizeTransitionType', () => {
|
|
10
|
+
it('returns exact canonical types unchanged', () => {
|
|
11
|
+
expect(normalizeTransitionType('fade')).toBe('fade');
|
|
12
|
+
expect(normalizeTransitionType('checkerboard')).toBe('checkerboard');
|
|
13
|
+
expect(normalizeTransitionType('slideLeft')).toBe('slideLeft');
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('resolves case / punctuation variants of canonical types', () => {
|
|
17
|
+
// These have NO explicit alias entry — they round-trip through the
|
|
18
|
+
// case-insensitive `NORMALIZED_TRANSITION_TYPES` fallback. This guards the
|
|
19
|
+
// removal of the redundant self-aliases (checkerboard/shape/uncover).
|
|
20
|
+
expect(normalizeTransitionType('Checkerboard')).toBe('checkerboard');
|
|
21
|
+
expect(normalizeTransitionType('CHECKERBOARD')).toBe('checkerboard');
|
|
22
|
+
expect(normalizeTransitionType('Shape')).toBe('shape');
|
|
23
|
+
expect(normalizeTransitionType('Uncover')).toBe('uncover');
|
|
24
|
+
expect(normalizeTransitionType('fly-through')).toBe('flyThrough');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('resolves explicit aliases (including plurals)', () => {
|
|
28
|
+
// Aliases only fire for spellings that are NOT themselves canonical types
|
|
29
|
+
// (an exact type returns early). `ferris` IS a canonical type, so its
|
|
30
|
+
// sameness with `ferrisWheel` comes from the visual map, not here.
|
|
31
|
+
expect(normalizeTransitionType('none')).toBe('cut');
|
|
32
|
+
expect(normalizeTransitionType('checkerboards')).toBe('checkerboard');
|
|
33
|
+
expect(normalizeTransitionType('shapes')).toBe('shape');
|
|
34
|
+
expect(normalizeTransitionType('flythru')).toBe('flyThrough');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('returns null for unknown values', () => {
|
|
38
|
+
expect(normalizeTransitionType('definitely-not-a-transition')).toBeNull();
|
|
39
|
+
expect(normalizeTransitionType('')).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('every canonical type normalizes to itself', () => {
|
|
43
|
+
for (const type of TRANSITION_TYPES) {
|
|
44
|
+
expect(normalizeTransitionType(type), type).toBe(type);
|
|
45
|
+
expect(isTransitionType(type), type).toBe(true);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('normalizeTransitionDirection', () => {
|
|
51
|
+
it('resolves cardinal directions and shorthands', () => {
|
|
52
|
+
expect(normalizeTransitionDirection('left')).toBe('left');
|
|
53
|
+
expect(normalizeTransitionDirection('R')).toBe('right');
|
|
54
|
+
expect(normalizeTransitionDirection('horizontal')).toBe('horizontal');
|
|
55
|
+
expect(normalizeTransitionDirection('v')).toBe('vertical');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('returns null for unknown directions', () => {
|
|
59
|
+
expect(normalizeTransitionDirection('sideways')).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
});
|