Skip to content

Commit

Permalink
test: 💍 dotlottie utils module
Browse files Browse the repository at this point in the history
  • Loading branch information
theashraf committed Aug 3, 2023
1 parent afb23cc commit 0655988
Show file tree
Hide file tree
Showing 3 changed files with 457 additions and 7 deletions.
14 changes: 7 additions & 7 deletions packages/dotlottie-js/src/common/dotlottie-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export enum ErrorCodes {
INVALID_URL = 'INVALID_URL',
}

class DotLottieError extends Error {
export class DotLottieError extends Error {
public code: ErrorCodes;

public constructor(message: string, code: ErrorCodes) {
Expand All @@ -44,7 +44,7 @@ export function dataUrlFromU8(uint8Data: Uint8Array): string {
base64 = Buffer.from(uint8Data).toString('base64');
} else {
// Using window.btoa for browser environments
const binaryString = strFromU8(uint8Data, false);
const binaryString = Array.prototype.map.call(uint8Data, (byte: number) => String.fromCharCode(byte)).join('');

base64 = window.btoa(binaryString);
}
Expand Down Expand Up @@ -123,7 +123,7 @@ export async function getManifest(dotLottie: Uint8Array): Promise<Manifest> {
const unzippedManifest = unzipped[manifestFileName];

if (!(unzippedManifest instanceof Uint8Array)) {
throw new DotLottieError('Manifest not found', ErrorCodes.INVALID_DOTLOTTIE);
throw new DotLottieError('Invalid .lottie file, manifest.json is missing', ErrorCodes.INVALID_DOTLOTTIE);
}

return JSON.parse(strFromU8(unzippedManifest, false)) as Manifest;
Expand All @@ -144,7 +144,7 @@ export async function validateDotLottie(dotLottie: Uint8Array): Promise<{ error?
const manifestValidationResult = ManifestSchema.safeParse(manifest);

if (!manifestValidationResult.success) {
const error = manifestValidationResult.error.toString();
const error = `Invalid .lottie file, manifest.json structure is invalid, ${manifestValidationResult.error.toString()}`;

return { success: false, error };
}
Expand All @@ -162,8 +162,8 @@ export async function loadFromArrayBuffer(arrayBuffer: ArrayBuffer): Promise<Uin

const validationResult = await validateDotLottie(dotLottie);

if (!validationResult.success) {
throw new DotLottieError(validationResult.error ?? 'Invalid .lottie file', ErrorCodes.INVALID_DOTLOTTIE);
if (validationResult.error) {
throw new DotLottieError(validationResult.error, ErrorCodes.INVALID_DOTLOTTIE);
}

return dotLottie;
Expand All @@ -176,7 +176,7 @@ export async function loadFromArrayBuffer(arrayBuffer: ArrayBuffer): Promise<Uin
*/
export async function loadFromURL(src: string): Promise<Uint8Array> {
if (!isValidURL(src)) {
throw new DotLottieError('Invalid URL provided', ErrorCodes.INVALID_URL);
throw new DotLottieError('Invalid url provided for .lottie file', ErrorCodes.INVALID_URL);
}

const response = await fetch(src);
Expand Down
225 changes: 225 additions & 0 deletions packages/dotlottie-js/src/tests/dotlottie-utils-browser.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/**
* Copyright 2023 Design Barn Inc.
*/

/* eslint-disable @lottiefiles/import-filename-format */

import { zipSync, unzipSync } from 'fflate';

import type { AnimationData, Manifest } from '..';
import {
DotLottieError,
ErrorCodes,
dataUrlFromU8,
getAnimation,
getAnimations,
getImage,
getImages,
getManifest,
getTheme,
getThemes,
loadFromURL,
} from '..';

import dotLottieAnimation from './__fixtures__/simple/animation.lottie';
import dotLottieLottie1 from './__fixtures__/simple/animation/animations/lottie1.json';
import dotLottieManifest from './__fixtures__/simple/animation/manifest.json';
import dotLottieTheme from './__fixtures__/simple/animation/themes/theme1.lss';
import dotLottieAnimationWithImages from './__fixtures__/simple/big-merged-dotlottie.lottie';

describe('loadFromUrl', () => {
it('throws when url is not valid', async () => {
await expectAsync(loadFromURL('')).toBeRejectedWith(
new DotLottieError('Invalid url provided for .lottie file', ErrorCodes.INVALID_DOTLOTTIE),
);
});

it('throws an error if invalid content-type is returned', async () => {
spyOn(typeof window === 'undefined' ? global : window, 'fetch').and.returnValue(
Promise.resolve(new Response(dotLottieAnimation, { headers: { 'content-type': 'text/html' } })),
);

const dotLottieURL = 'https://lottiefiles.fake/animation/animation.lottie';

await expectAsync(loadFromURL(dotLottieURL)).toBeRejectedWith(
new DotLottieError(
'Invalid content type provided for .lottie file, expected application/zip',
ErrorCodes.INVALID_DOTLOTTIE,
),
);
});

it('loads a dotlottie from a url', async () => {
const fetchSpy = spyOn(typeof window === 'undefined' ? global : window, 'fetch').and.returnValue(
Promise.resolve(new Response(dotLottieAnimation, { headers: { 'content-type': 'application/zip' } })),
);

const dotLottieURL = 'https://lottiefiles.fake/animation/animation.lottie';

const dotLottie = await loadFromURL(dotLottieURL);

expect(dotLottie).toBeDefined();
expect(dotLottie).toBeInstanceOf(Uint8Array);

expect(fetchSpy).toHaveBeenCalledWith(dotLottieURL);
});

it('throws error if dotlottie with no manifest is loaded', async () => {
const data: Record<string, Uint8Array> = {};

// convert the lottie to uint8array
data['animations/lottie1.json'] = new TextEncoder().encode(JSON.stringify(dotLottieLottie1));

const dotLottieWithNoManifest = zipSync(data);

const fetchSpy = spyOn(typeof window === 'undefined' ? global : window, 'fetch').and.returnValue(
Promise.resolve(new Response(dotLottieWithNoManifest, { headers: { 'content-type': 'application/zip' } })),
);

const dotLottieURL = 'https://lottiefiles.fake/animation/animation.lottie';

await expectAsync(loadFromURL(dotLottieURL)).toBeRejectedWith(
new DotLottieError('Invalid .lottie file, manifest.json is missing', ErrorCodes.INVALID_DOTLOTTIE),
);

expect(fetchSpy).toHaveBeenCalledWith(dotLottieURL);
});

it('throws error if manifest.json has invalid structure', async () => {
const data: Record<string, Uint8Array> = {};

data['manifest.json'] = new TextEncoder().encode(
JSON.stringify({
version: '1.0',
revision: 1,
keywords: 'dotLottie',
author: 'LottieFiles',
generator: 'dotLottie-js_v2.0',
// animations array is missing
themes: [{ id: 'theme1', animations: ['lottie1'] }],
}),
);

const dotLottieWithInvalidManifest = zipSync(data);

const fetchSpy = spyOn(typeof window === 'undefined' ? global : window, 'fetch').and.returnValue(
Promise.resolve(new Response(dotLottieWithInvalidManifest, { headers: { 'content-type': 'application/zip' } })),
);

const dotLottieURL = 'https://lottiefiles.fake/animation/animation.lottie';

await expectAsync(loadFromURL(dotLottieURL)).toBeRejectedWithError(
/invalid .lottie file, manifest.json structure is invalid/iu,
);

expect(fetchSpy).toHaveBeenCalledWith(dotLottieURL);
});
});

describe('getManifest', () => {
it('return manifest from dotlottie', async () => {
const manifest = await getManifest(dotLottieAnimation);

expect(manifest).toEqual(dotLottieManifest as Manifest);
});
});

describe('getImage', () => {
it('throws error if image not found', async () => {
await expectAsync(getImage(dotLottieAnimationWithImages, 'invalid.png')).toBeRejectedWith(
new DotLottieError('File not found: images/invalid.png', ErrorCodes.ASSET_NOT_FOUND),
);
});
});

describe('getAnimation', () => {
it('throws error if animation not found', async () => {
await expectAsync(getAnimation(dotLottieAnimationWithImages, 'animation_id')).toBeRejectedWith(
new DotLottieError('File not found: animations/animation_id.json', ErrorCodes.ASSET_NOT_FOUND),
);
});

it('get animation by id', async () => {
const animation = await getAnimation(dotLottieAnimation, 'lottie1');

expect(animation).toEqual(dotLottieLottie1 as AnimationData);
});
});

describe('getTheme', () => {
it('throws error if animation not found', async () => {
await expectAsync(getTheme(dotLottieAnimationWithImages, 'theme_id')).toBeRejectedWith(
new DotLottieError('File not found: themes/theme_id.lss', ErrorCodes.ASSET_NOT_FOUND),
);
});

it('gets theme by id', async () => {
const theme = await getTheme(dotLottieAnimation, 'theme1');

expect(theme).toEqual(dotLottieTheme);
});
});

describe('getImages', () => {
it('returns a map of images', async () => {
const images = await getImages(dotLottieAnimationWithImages);

const unzippedDotLottie = unzipSync(dotLottieAnimationWithImages);
const expectedImages: Record<string, string> = {};

// eslint-disable-next-line guard-for-in
for (const key in unzippedDotLottie) {
const data = unzippedDotLottie[key];

if (key.startsWith('images/') && data) {
expectedImages[key.replace('images/', '')] = dataUrlFromU8(data);
}
}

expect(images).toEqual(expectedImages);
});

it('returns a map of images with filter', async () => {
const images = await getImages(dotLottieAnimationWithImages, (file) => file.name.startsWith('images/invalid'));

expect(images).toEqual({});
});
});

describe('getThemes', () => {
it('returns a map of themes', async () => {
const themes = await getThemes(dotLottieAnimation);

const expectedThemes = {
theme1: dotLottieTheme,
};

expect(themes).toEqual(expectedThemes);
});

it('returns a map of themes with filter', async () => {
const themes = await getThemes(dotLottieAnimation, (file) => file.name.startsWith('themes/invalid'));

expect(themes).toEqual({});
});
});

describe('getAnimations', () => {
it('returns a map of animations', async () => {
const animations = await getAnimations(dotLottieAnimation);

const expectedAnimations = {
lottie1: dotLottieLottie1 as AnimationData,
};

expect(animations).toEqual(expectedAnimations);
});

it('returns a map of animations with filter', async () => {
const animations = await getAnimations(dotLottieAnimation, { inlineAssets: false }, (file) =>
file.name.startsWith('animations/invalid'),
);

expect(animations).toEqual({});
});
});
Loading

0 comments on commit 0655988

Please sign in to comment.