From c6fa97962a4c6389e1c39ff999d0e3cbbeb79d0f Mon Sep 17 00:00:00 2001 From: Abdelrahman Ashraf Date: Mon, 4 Nov 2024 21:46:16 +0700 Subject: [PATCH] refactor: add tests && update LottieThemeCommon toString method --- .changeset/neat-carrots-judge.md | 5 + .../src/__tests__/__fixtures__/ball.json | 738 ++++++++++++++++++ .../v2/__tests__/browser/dotlottie.spec.ts | 166 +++- .../src/v2/__tests__/node/dotlottie.spec.ts | 159 +++- packages/dotlottie-js/src/v2/common/theme.ts | 5 +- 5 files changed, 1062 insertions(+), 11 deletions(-) create mode 100644 .changeset/neat-carrots-judge.md create mode 100644 packages/dotlottie-js/src/__tests__/__fixtures__/ball.json diff --git a/.changeset/neat-carrots-judge.md b/.changeset/neat-carrots-judge.md new file mode 100644 index 0000000..4df43bf --- /dev/null +++ b/.changeset/neat-carrots-judge.md @@ -0,0 +1,5 @@ +--- +'@dotlottie/dotlottie-js': patch +--- + +refactor: add tests && update LottieThemeCommon toString method diff --git a/packages/dotlottie-js/src/__tests__/__fixtures__/ball.json b/packages/dotlottie-js/src/__tests__/__fixtures__/ball.json new file mode 100644 index 0000000..49dc5a1 --- /dev/null +++ b/packages/dotlottie-js/src/__tests__/__fixtures__/ball.json @@ -0,0 +1,738 @@ +{ + "v": "4.8.0", + "meta": { + "g": "LottieFiles AE ", + "a": "", + "k": "", + "d": "", + "tc": "" + }, + "fr": 30, + "ip": 0, + "op": 61, + "w": 1080, + "h": 1080, + "nm": "B", + "assets": [], + "layers": [ + { + "ind": 1, + "ty": 4, + "nm": "B", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100 + }, + "r": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.833 + ], + "y": [ + 0.833 + ] + }, + "o": { + "x": [ + 0.167 + ], + "y": [ + 0.167 + ] + }, + "t": 0, + "s": [ + 0 + ] + }, + { + "t": 58, + "s": [ + 349.032 + ] + } + ] + }, + "p": { + "s": true, + "x": { + "a": 0, + "k": 540 + }, + "y": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.67 + ], + "y": [ + 0.343 + ] + }, + "o": { + "x": [ + 0.33 + ], + "y": [ + 0 + ] + }, + "t": 0, + "s": [ + 152 + ] + }, + { + "i": { + "x": [ + 0.67 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.33 + ], + "y": [ + 0 + ] + }, + "t": 15, + "s": [ + 915 + ] + }, + { + "i": { + "x": [ + 0.67 + ], + "y": [ + 0.343 + ] + }, + "o": { + "x": [ + 0.33 + ], + "y": [ + 0 + ] + }, + "t": 30, + "s": [ + 152 + ] + }, + { + "i": { + "x": [ + 0.67 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.33 + ], + "y": [ + 0 + ] + }, + "t": 45, + "s": [ + 915 + ] + }, + { + "t": 60, + "s": [ + 152 + ] + } + ] + } + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ] + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ] + } + }, + "ao": 0, + "shapes": [ + { + "ty": "rc", + "d": 1, + "s": { + "a": 0, + "k": [ + 270, + 270 + ] + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "r": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 15, + "s": [ + 135 + ] + }, + { + "i": { + "x": [ + 0.667 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 30, + "s": [ + 33.75 + ] + }, + { + "i": { + "x": [ + 0.667 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 45, + "s": [ + 33.75 + ] + }, + { + "t": 60, + "s": [ + 135 + ] + } + ] + }, + "nm": "S" + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.961, + 0.761, + 0.267, + 1 + ], + "sid": "ball_color" + }, + "o": { + "a": 0, + "k": 100 + }, + "r": 1, + "nm": "C" + } + ], + "ip": 0, + "op": 61, + "st": 0 + }, + { + "ind": 2, + "ty": 4, + "nm": "S", + "sr": 1, + "ks": { + "o": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 0, + "s": [ + 10 + ] + }, + { + "i": { + "x": [ + 0.667 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 15, + "s": [ + 100 + ] + }, + { + "i": { + "x": [ + 0.667 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 30, + "s": [ + 10 + ] + }, + { + "i": { + "x": [ + 0.667 + ], + "y": [ + 1 + ] + }, + "o": { + "x": [ + 0.333 + ], + "y": [ + 0 + ] + }, + "t": 45, + "s": [ + 100 + ] + }, + { + "t": 60, + "s": [ + 10 + ] + } + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "p": { + "a": 0, + "k": [ + 540, + 1051, + 0 + ] + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ] + }, + "s": { + "a": 0, + "k": [ + 50, + 50, + 100 + ] + } + }, + "ao": 0, + "shapes": [ + { + "ty": "rc", + "d": 1, + "s": { + "a": 1, + "k": [ + { + "i": { + "x": [ + 0.667, + 0.667 + ], + "y": [ + 1, + 1 + ] + }, + "o": { + "x": [ + 0.333, + 0.333 + ], + "y": [ + 0, + 0 + ] + }, + "t": 0, + "s": [ + 270, + 13.5 + ] + }, + { + "i": { + "x": [ + 0.667, + 0.667 + ], + "y": [ + 1, + 1 + ] + }, + "o": { + "x": [ + 0.333, + 0.333 + ], + "y": [ + 0, + 0 + ] + }, + "t": 15, + "s": [ + 540, + 13.5 + ] + }, + { + "i": { + "x": [ + 0.667, + 0.667 + ], + "y": [ + 1, + 1 + ] + }, + "o": { + "x": [ + 0.333, + 0.333 + ], + "y": [ + 0, + 0 + ] + }, + "t": 30, + "s": [ + 270, + 13.5 + ] + }, + { + "i": { + "x": [ + 0.667, + 0.667 + ], + "y": [ + 1, + 1 + ] + }, + "o": { + "x": [ + 0.333, + 0.333 + ], + "y": [ + 0, + 0 + ] + }, + "t": 45, + "s": [ + 540, + 13.5 + ] + }, + { + "t": 60, + "s": [ + 270, + 13.5 + ] + } + ] + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "r": { + "a": 0, + "k": 790 + }, + "nm": "R" + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.114, + 0.114, + 0.114, + 1 + ], + "sid": "bg_color" + }, + "o": { + "a": 0, + "k": 100 + }, + "r": 1, + "nm": "F" + } + ], + "ip": 0, + "op": 61, + "st": 0 + }, + { + "ind": 3, + "ty": 4, + "nm": "B", + "sr": 1, + "ks": { + "o": { + "a": 0, + "k": 100 + }, + "r": { + "a": 0, + "k": 0 + }, + "p": { + "a": 0, + "k": [ + 540, + 540, + 0 + ] + }, + "a": { + "a": 0, + "k": [ + 0, + 0, + 0 + ] + }, + "s": { + "a": 0, + "k": [ + 100, + 100, + 100 + ] + } + }, + "ao": 0, + "shapes": [ + { + "ty": "gr", + "it": [ + { + "ty": "rc", + "d": 1, + "s": { + "a": 0, + "k": [ + 1080, + 1080 + ] + }, + "p": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "nm": "R" + }, + { + "ty": "fl", + "c": { + "a": 0, + "k": [ + 0.153, + 0.153, + 0.153, + 1 + ] + }, + "o": { + "a": 0, + "k": 100 + }, + "r": 1, + "nm": "F" + }, + { + "ty": "tr", + "p": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "a": { + "a": 0, + "k": [ + 0, + 0 + ] + }, + "s": { + "a": 0, + "k": [ + 100, + 100 + ] + }, + "r": { + "a": 0, + "k": 0 + }, + "o": { + "a": 0, + "k": 100 + }, + "sk": { + "a": 0, + "k": 0 + }, + "sa": { + "a": 0, + "k": 0 + }, + "nm": "T" + } + ], + "nm": "G" + } + ], + "ip": 0, + "op": 61, + "st": 0 + } + ], + "markers": [] +} \ No newline at end of file diff --git a/packages/dotlottie-js/src/v2/__tests__/browser/dotlottie.spec.ts b/packages/dotlottie-js/src/v2/__tests__/browser/dotlottie.spec.ts index 817ef8f..261d674 100644 --- a/packages/dotlottie-js/src/v2/__tests__/browser/dotlottie.spec.ts +++ b/packages/dotlottie-js/src/v2/__tests__/browser/dotlottie.spec.ts @@ -5,11 +5,13 @@ /* eslint-disable @lottiefiles/import-filename-format */ import type { Animation as AnimationType } from '@lottie-animation-community/lottie-types'; -import { unzipSync } from 'fflate'; +import { strFromU8, unzipSync } from 'fflate'; import { Base64 } from 'js-base64'; import { describe, test, expect, vi } from 'vitest'; import pkg from '../../../../package.json'; +import BALL_ANIMATION_DATA from '../../../__tests__/__fixtures__/ball.json'; +import BULL_ANIMATION_DATA from '../../../__tests__/__fixtures__/bull.json'; import bullData from '../../../__tests__/__fixtures__/image-asset-optimization/bull.json'; import IMAGE_ANIMATION_1_DATA from '../../../__tests__/__fixtures__/image-asset-optimization/image-animation-layer-1.json'; import IMAGE_ANIMATION_5_DATA from '../../../__tests__/__fixtures__/image-asset-optimization/image-animation-layer-2-3-4-5.json'; @@ -331,8 +333,8 @@ describe('download', () => { const dotlottie = new DotLottie(); - await expect( - await dotlottie + expect( + dotlottie .addAnimation({ id: 'test_animation', data: animationData as unknown as AnimationType, @@ -495,9 +497,6 @@ describe('toBase64', () => { .toBase64(); const actualArrayBuffer = Base64.toUint8Array(dataURL).buffer; - - expect(actualArrayBuffer).toBeInstanceOf(ArrayBuffer); - const actualContent = unzipSync(new Uint8Array(actualArrayBuffer)); expect(Object.keys(actualContent).length).toBeGreaterThan(0); @@ -768,3 +767,158 @@ describe('build', () => { fetchSpy.mockRestore(); }); }); + +describe('theming', () => { + test('adds a global theme to the dotlottie file', async () => { + const dotlottie = new DotLottie(); + + dotlottie.addAnimation({ + id: 'ball', + data: structuredClone(BALL_ANIMATION_DATA) as unknown as AnimationData, + }); + + dotlottie.addTheme({ + id: 'light', + data: { + rules: [ + { + id: 'ball-color', + type: 'Color', + value: [0, 1, 0, 1], + }, + ], + }, + }); + + await dotlottie.build(); + + expect(dotlottie.manifest).toEqual({ + version: '2', + generator: `${pkg.name}@${pkg.version}`, + animations: [{ id: 'ball' }], + themes: [{ id: 'light' }], + }); + + expect(dotlottie.animations[0]?.themes.length).toBe(0); + }); + + test('scopes a theme to an animation', async () => { + const dotlottie = new DotLottie(); + + dotlottie.addAnimation({ + id: 'ball', + data: structuredClone(BALL_ANIMATION_DATA) as unknown as AnimationData, + }); + + dotlottie.addTheme({ + id: 'light', + data: { + rules: [{ id: 'ball-color', type: 'Color', value: [0, 1, 0, 1] }], + }, + }); + + dotlottie.scopeTheme({ + animationId: 'ball', + themeId: 'light', + }); + + await dotlottie.build(); + + expect(dotlottie.manifest).toEqual({ + version: '2', + generator: `${pkg.name}@${pkg.version}`, + animations: [{ id: 'ball', themes: ['light'] }], + themes: [{ id: 'light' }], + }); + + expect(dotlottie.animations[0]?.themes.length).toBe(1); + expect(dotlottie.animations[0]?.themes[0]?.id).toBe('light'); + }); + + test('throws an error if the theme does not exist', async () => { + const dotlottie = new DotLottie(); + + expect(() => dotlottie.scopeTheme({ animationId: 'ball', themeId: 'light' })).toThrow( + 'Failed to find theme with id light', + ); + }); + + test('throws an error if the animation does not exist', async () => { + const dotlottie = new DotLottie(); + + dotlottie.addTheme({ + id: 'light', + data: { + rules: [{ id: 'ball-color', type: 'Color', value: [0, 1, 0, 1] }], + }, + }); + + expect(() => dotlottie.scopeTheme({ animationId: 'ball', themeId: 'light' })).toThrow( + 'Failed to find animation with id ball', + ); + }); + + test('themes assets are properly exported in the .lottie file', async () => { + const dotlottie = new DotLottie(); + + dotlottie.addAnimation({ + id: 'ball', + data: structuredClone(BALL_ANIMATION_DATA) as unknown as AnimationData, + }); + + dotlottie.addAnimation({ + id: 'bull', + data: structuredClone(BULL_ANIMATION_DATA) as unknown as AnimationData, + }); + + dotlottie.addTheme({ + id: 'light', + name: 'Light Theme', + data: { + rules: [{ id: 'ball-color', type: 'Color', value: [0, 1, 0, 1] }], + }, + }); + + dotlottie.addTheme({ + id: 'dark', + data: { + rules: [{ id: 'bull-color', type: 'Color', value: [1, 0, 0, 1] }], + }, + }); + + dotlottie.scopeTheme({ + animationId: 'bull', + themeId: 'dark', + }); + + await dotlottie.build(); + + const dotLottieFile = await dotlottie.toArrayBuffer(); + + const content = unzipSync(new Uint8Array(dotLottieFile)); + + expect(Object.keys(content)).toEqual([ + 'manifest.json', + 'a/ball.json', + 'a/bull.json', + 'i/image_1.png', + 'i/image_2.png', + 'i/image_3.png', + 'i/image_4.png', + 'i/image_5.png', + 't/light.json', + 't/dark.json', + ]); + + expect(strFromU8(content['t/light.json'] as Uint8Array)).toEqual(JSON.stringify(dotlottie.themes[0]?.data)); + expect(strFromU8(content['a/ball.json'] as Uint8Array)).toEqual(JSON.stringify(dotlottie.animations[0]?.data)); + expect(strFromU8(content['a/bull.json'] as Uint8Array)).toEqual(JSON.stringify(dotlottie.animations[1]?.data)); + expect(strFromU8(content['manifest.json'] as Uint8Array)).toEqual(JSON.stringify(dotlottie.manifest)); + expect(dotlottie.manifest).toEqual({ + version: '2', + generator: `${pkg.name}@${pkg.version}`, + animations: [{ id: 'ball' }, { id: 'bull', themes: ['dark'] }], + themes: [{ id: 'light', name: 'Light Theme' }, { id: 'dark' }], + }); + }); +}); diff --git a/packages/dotlottie-js/src/v2/__tests__/node/dotlottie.spec.ts b/packages/dotlottie-js/src/v2/__tests__/node/dotlottie.spec.ts index 13aef96..44d966b 100644 --- a/packages/dotlottie-js/src/v2/__tests__/node/dotlottie.spec.ts +++ b/packages/dotlottie-js/src/v2/__tests__/node/dotlottie.spec.ts @@ -5,11 +5,13 @@ /* eslint-disable @lottiefiles/import-filename-format */ import type { Animation as AnimationType } from '@lottie-animation-community/lottie-types'; -import { unzipSync } from 'fflate'; +import { strFromU8, unzipSync } from 'fflate'; import { Base64 } from 'js-base64'; import { describe, test, expect, vi } from 'vitest'; import pkg from '../../../../package.json'; +import BALL_ANIMATION_DATA from '../../../__tests__/__fixtures__/ball.json'; +import BULL_ANIMATION_DATA from '../../../__tests__/__fixtures__/bull.json'; import bullData from '../../../__tests__/__fixtures__/image-asset-optimization/bull.json'; import IMAGE_ANIMATION_1_DATA from '../../../__tests__/__fixtures__/image-asset-optimization/image-animation-layer-1.json'; import IMAGE_ANIMATION_5_DATA from '../../../__tests__/__fixtures__/image-asset-optimization/image-animation-layer-2-3-4-5.json'; @@ -765,3 +767,158 @@ describe('build', () => { fetchSpy.mockRestore(); }); }); + +describe('theming', () => { + test('adds a global theme to the dotlottie file', async () => { + const dotlottie = new DotLottie(); + + dotlottie.addAnimation({ + id: 'ball', + data: structuredClone(BALL_ANIMATION_DATA) as unknown as AnimationData, + }); + + dotlottie.addTheme({ + id: 'light', + data: { + rules: [ + { + id: 'ball-color', + type: 'Color', + value: [0, 1, 0, 1], + }, + ], + }, + }); + + await dotlottie.build(); + + expect(dotlottie.manifest).toEqual({ + version: '2', + generator: `${pkg.name}@${pkg.version}`, + animations: [{ id: 'ball' }], + themes: [{ id: 'light' }], + }); + + expect(dotlottie.animations[0]?.themes.length).toBe(0); + }); + + test('scopes a theme to an animation', async () => { + const dotlottie = new DotLottie(); + + dotlottie.addAnimation({ + id: 'ball', + data: structuredClone(BALL_ANIMATION_DATA) as unknown as AnimationData, + }); + + dotlottie.addTheme({ + id: 'light', + data: { + rules: [{ id: 'ball-color', type: 'Color', value: [0, 1, 0, 1] }], + }, + }); + + dotlottie.scopeTheme({ + animationId: 'ball', + themeId: 'light', + }); + + await dotlottie.build(); + + expect(dotlottie.manifest).toEqual({ + version: '2', + generator: `${pkg.name}@${pkg.version}`, + animations: [{ id: 'ball', themes: ['light'] }], + themes: [{ id: 'light' }], + }); + + expect(dotlottie.animations[0]?.themes.length).toBe(1); + expect(dotlottie.animations[0]?.themes[0]?.id).toBe('light'); + }); + + test('throws an error if the theme does not exist', async () => { + const dotlottie = new DotLottie(); + + expect(() => dotlottie.scopeTheme({ animationId: 'ball', themeId: 'light' })).toThrow( + 'Failed to find theme with id light', + ); + }); + + test('throws an error if the animation does not exist', async () => { + const dotlottie = new DotLottie(); + + dotlottie.addTheme({ + id: 'light', + data: { + rules: [{ id: 'ball-color', type: 'Color', value: [0, 1, 0, 1] }], + }, + }); + + expect(() => dotlottie.scopeTheme({ animationId: 'ball', themeId: 'light' })).toThrow( + 'Failed to find animation with id ball', + ); + }); + + test('themes assets are properly exported in the .lottie file', async () => { + const dotlottie = new DotLottie(); + + dotlottie.addAnimation({ + id: 'ball', + data: structuredClone(BALL_ANIMATION_DATA) as unknown as AnimationData, + }); + + dotlottie.addAnimation({ + id: 'bull', + data: structuredClone(BULL_ANIMATION_DATA) as unknown as AnimationData, + }); + + dotlottie.addTheme({ + id: 'light', + name: 'Light Theme', + data: { + rules: [{ id: 'ball-color', type: 'Color', value: [0, 1, 0, 1] }], + }, + }); + + dotlottie.addTheme({ + id: 'dark', + data: { + rules: [{ id: 'bull-color', type: 'Color', value: [1, 0, 0, 1] }], + }, + }); + + dotlottie.scopeTheme({ + animationId: 'bull', + themeId: 'dark', + }); + + await dotlottie.build(); + + const dotLottieFile = await dotlottie.toArrayBuffer(); + + const content = unzipSync(new Uint8Array(dotLottieFile)); + + expect(Object.keys(content)).toEqual([ + 'manifest.json', + 'a/ball.json', + 'a/bull.json', + 'i/image_1.png', + 'i/image_2.png', + 'i/image_3.png', + 'i/image_4.png', + 'i/image_5.png', + 't/light.json', + 't/dark.json', + ]); + + expect(strFromU8(content['t/light.json'] as Uint8Array)).toEqual(JSON.stringify(dotlottie.themes[0]?.data)); + expect(strFromU8(content['a/ball.json'] as Uint8Array)).toEqual(JSON.stringify(dotlottie.animations[0]?.data)); + expect(strFromU8(content['a/bull.json'] as Uint8Array)).toEqual(JSON.stringify(dotlottie.animations[1]?.data)); + expect(strFromU8(content['manifest.json'] as Uint8Array)).toEqual(JSON.stringify(dotlottie.manifest)); + expect(dotlottie.manifest).toEqual({ + version: '2', + generator: `${pkg.name}@${pkg.version}`, + animations: [{ id: 'ball' }, { id: 'bull', themes: ['dark'] }], + themes: [{ id: 'light', name: 'Light Theme' }, { id: 'dark' }], + }); + }); +}); diff --git a/packages/dotlottie-js/src/v2/common/theme.ts b/packages/dotlottie-js/src/v2/common/theme.ts index d583fbf..19524b3 100644 --- a/packages/dotlottie-js/src/v2/common/theme.ts +++ b/packages/dotlottie-js/src/v2/common/theme.ts @@ -73,10 +73,7 @@ export class LottieThemeCommon { } public async toString(): Promise { - return JSON.stringify({ - id: this._id, - rules: this._data.rules, - }); + return JSON.stringify(this._data); } private _requireValidId(id: string | undefined): asserts id is string {