Skip to content

Commit

Permalink
feat: 🎸 implement more user-friendly error handling in slides
Browse files Browse the repository at this point in the history
When building a slide it will check all the semantics of slide
declaration and all the references shapes <-> animations to prevent
errors when showing off a presentation. This way, you will get all the
errors of missed shapes or animations ahead-of-time.
  • Loading branch information
ghaiklor committed Apr 8, 2020
1 parent fe5b05f commit c494c26
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 52 deletions.
5 changes: 4 additions & 1 deletion packages/kittik-slide/spec/AnimationBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ describe('animation builder', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
AnimationBuilder.start('Nonsense').end();
}).toThrow('Animation "Nonsense" you tried to build does not exist');
}).toThrow(
'You tried to build an animation with the type "Nonsense". ' +
'But the animation of this type is not implemented or you made a typo.'
);
});
});
5 changes: 4 additions & 1 deletion packages/kittik-slide/spec/ShapeBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ describe('shape builder', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
ShapeBuilder.start('Nonsense').end();
}).toThrow('Shape "Nonsense" you tried to build does not exist');
}).toThrow(
'You tried to build a shape with the type "Nonsense". ' +
'But the shape of this type is not implemented or you made a typo.'
);
});
});
82 changes: 54 additions & 28 deletions packages/kittik-slide/spec/Slide.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,42 +84,54 @@ describe('slide', () => {

const cursor = new Canvas();

expect(
() => new Slide(
cursor, {
name: 'Test',
shapes: [{ name: 'Test', type: 'unknown' }],
order: [{ shape: 'Test' }]
}
)
).toThrow('Shape "Test" (unknown) is unknown for me, maybe you made a typo?');
expect(() => new Slide(
cursor,
{
name: 'Test',
shapes: [{ name: 'Test', type: 'unknown' }],
order: [{ shape: 'Test' }]
}
)).toThrow(
'You have specified a shape with the name "Test" in slide "Test". ' +
'This shape has an unknown type "unknown". ' +
'Maybe you made a typo in "type" or tried to use a shape we do not have implemented.'
);
});

it('should properly throw an error if animation type is unknown', () => {
expect.hasAssertions();

const cursor = new Canvas();
expect(
() => new Slide(
cursor, {
name: 'Test',
shapes: [{ name: 'Test', type: 'Text' }],
animations: [{ name: 'Test', type: 'unknown' }],
order: [{ shape: 'Test' }]
}
)
).toThrow('Animation "Test" (unknown) is unknown for me, maybe you made a typo?');

expect(() => new Slide(
cursor,
{
name: 'Test',
shapes: [{ name: 'Test', type: 'Text' }],
animations: [{ name: 'Test', type: 'unknown' }],
order: [{ shape: 'Test' }]
}
)).toThrow(
'You have specified an animation with the name "Test" in slide "Test". ' +
'This animation has an unknown type "unknown". ' +
'Maybe you made a typo in "type" or tried to use an animation we do not have implemented.'
);
});

it('should properly throw an error if trying to use shape name in ordering that does not exist', async () => {
expect.hasAssertions();

const cursor = new Canvas();
const slide = new Slide(cursor, { name: 'Test', shapes: [], order: [{ shape: 'Not Exists' }] });
const slide = new Slide(cursor, {
name: 'Test',
shapes: [],
order: [{ shape: 'Not Exists' }]
});

await expect(slide.render()).rejects.toThrow(
'You specified shape "Not Exists" in slide "Test" as part of ordering, ' +
'but it does not exist in shapes declaration.'
'but it does not exist in shapes declaration for the slide. ' +
'Maybe you forgot to create a shape you want to order or it is a typo in ordering itself.'
);
});

Expand Down Expand Up @@ -214,7 +226,8 @@ describe('slide', () => {
it('should properly instantiate an empty slide instance when nothing is passed but an empty arrays', () => {
expect.hasAssertions();

const slide = new Slide(null, { name: 'Test', order: [], shapes: [] });
const cursor = new Canvas();
const slide = new Slide(cursor, { name: 'Test', order: [], shapes: [] });
expect(slide.cursor).toBeInstanceOf(Canvas);
expect(slide.shapes.size).toBe(0);
expect(slide.animations.size).toBe(0);
Expand All @@ -224,29 +237,42 @@ describe('slide', () => {
it('should properly throw an error when trying to add shape that is already added', () => {
expect.hasAssertions();

const slide = new Slide(null, { name: 'Test', shapes: [{ name: 'Test', type: 'Text' }], order: [] });
expect(() => slide.addShape('Test', Text.create())).toThrow('Shape "Test" already exists in slide');
const canvas = new Canvas();
const slide = new Slide(canvas, { name: 'Test', shapes: [{ name: 'Test', type: 'Text' }], order: [] });

expect(() => slide.addShape('Test', Text.create())).toThrow(
'You are trying to add shape with the name "Test" into the slide "Test". ' +
'But this shape already exists in slide "Test".'
);
});

it('should properly throw an error when trying to add animation that is already added', () => {
expect.hasAssertions();

const slide = new Slide(null, {
const canvas = new Canvas();
const slide = new Slide(canvas, {
name: 'Test',
shapes: [],
order: [],
animations: [{ name: 'Test', type: 'Print' }]
});

expect(() => slide.addAnimation('Test', Print.create())).toThrow('Animation "Test" already exists in slide');
expect(() => slide.addAnimation('Test', Print.create())).toThrow(
'You are trying to add animation with the name "Test" into the slide "Test". ' +
'But this animation already exists in slide "Test".'
);
});

it('should properly throw an error when trying to add ordering for the shape that is already added', () => {
expect.hasAssertions();

const slide = new Slide(null, { name: 'Test', shapes: [], order: [{ shape: 'Test' }] });
const canvas = new Canvas();
const slide = new Slide(canvas, { name: 'Test', shapes: [], order: [{ shape: 'Test' }] });

expect(() => slide.addOrder('Test')).toThrow(
'You already have an ordering for shape "Test" in slide "Test"'
'You already have specified an ordering for shape "Test" in slide "Test". ' +
'Adding another one with the same name does not make any sense. ' +
'Did you make a typo in shape name or forgot that you already added a shape to ordering?'
);
});

Expand Down
5 changes: 4 additions & 1 deletion packages/kittik-slide/src/animation/AnimationBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ export class AnimationBuilder implements AnimationObject {
public end (): Animationable {
const ctr = ANIMATIONS.get(this.type);
if (typeof ctr === 'undefined') {
throw new Error(`Animation "${this.type}" you tried to build does not exist`);
throw new Error(
`You tried to build an animation with the type "${this.type}". ` +
'But the animation of this type is not implemented or you made a typo.'
);
}

return ctr.fromObject(this);
Expand Down
5 changes: 4 additions & 1 deletion packages/kittik-slide/src/shape/ShapeBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ export class ShapeBuilder implements ShapeObject {
public end (): ShapeRenderable {
const ctr = SHAPES.get(this.type);
if (typeof ctr === 'undefined') {
throw new Error(`Shape "${this.type}" you tried to build does not exist`);
throw new Error(
`You tried to build a shape with the type "${this.type}". ` +
'But the shape of this type is not implemented or you made a typo.'
);
}

return ctr.fromObject(this);
Expand Down
62 changes: 42 additions & 20 deletions packages/kittik-slide/src/slide/Slide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,59 +47,78 @@ export class Slide {
}
}

public static create (cursor: Canvas, declaration: SlideDeclaration): Slide {
public static create (cursor?: Canvas, declaration?: SlideDeclaration): Slide {
return new this(cursor, declaration);
}

public static fromObject (obj: SlideDeclaration, cursor: Canvas): Slide {
public static fromObject (obj: SlideDeclaration, cursor?: Canvas): Slide {
return this.create(cursor, obj);
}

public static fromJSON (json: string, cursor: Canvas): Slide {
public static fromJSON (json: string, cursor?: Canvas): Slide {
return this.fromObject(JSON.parse(json), cursor);
}

public addShape (name: string, shape: ShapeRenderable, toOverride = false): void {
if (this.shapes.has(name) && !toOverride) {
throw new Error(`Shape "${name}" already exists in slide "${this.name}"`);
throw new Error(
`You are trying to add shape with the name "${name}" into the slide "${this.name}". ` +
`But this shape already exists in slide "${this.name}".`
);
}

this.shapes.set(name, shape);
}

public addAnimation (name: string, animation: Animationable, toOverride = false): void {
if (this.animations.has(name) && !toOverride) {
throw new Error(`Animation "${name}" already exists in slide "${this.name}"`);
throw new Error(
`You are trying to add animation with the name "${name}" into the slide "${this.name}". ` +
`But this animation already exists in slide "${this.name}".`
);
}

this.animations.set(name, animation);
}

public addOrder (shape: string, animations: string[] = []): void {
if (this.order.some((order: OrderDeclaration) => order.shape === shape)) {
throw new Error(`You already have an ordering for shape "${shape}" in slide "${this.name}"`);
if (this.order.some((order) => order.shape === shape)) {
throw new Error(
`You already have specified an ordering for shape "${shape}" in slide "${this.name}". ` +
'Adding another one with the same name does not make any sense. ' +
'Did you make a typo in shape name or forgot that you already added a shape to ordering?'
);
}

const unknownAnimations = animations.filter((animation) => !this.animations.has(animation));
if (unknownAnimations.length > 0) {
throw new Error(
`You have provided animations for the shape "${shape}" in slide "${this.name}". ` +
`But, some of them could not be found in the slide "${this.name}". ` +
`These animations are: [${unknownAnimations.join(', ')}]. ` +
`Please, check if the animations from the list are declared in slide "${this.name}".`
);
}

this.order.push({ animations, shape });
}

// eslint-disable-next-line max-statements
public async render (): Promise<void> {
const { shapes } = this;
const { animations } = this;
const { animations, shapes } = this;
const shapesToRender: ShapeRenderable[] = [];
const sequence: Array<() => void> = [];
const onTick = (): void => this.renderShapes(shapesToRender);

// We need to re-render shapes each time when some of the animations made a tick
// Hence, made an update in shape properties that we need to reflect on canvas
animations.forEach((animation: Animationable) => animation.on('tick', () => this.renderShapes(shapesToRender)));
animations.forEach((animation) => animation.on('tick', onTick));

for (const order of this.order) {
const shapeToRender = shapes.get(order.shape);
if (typeof shapeToRender === 'undefined') {
throw new Error(
`You specified shape "${order.shape}" in slide "${this.name}" ` +
'as part of ordering, but it does not exist in shapes declaration.\n' +
'as part of ordering, but it does not exist in shapes declaration for the slide. ' +
'Maybe you forgot to create a shape you want to order or it is a typo in ordering itself.'
);
}
Expand All @@ -117,9 +136,9 @@ export class Slide {
}

// When all of the rendering and animation is done - we can freely remove the listeners from animations
sequence.push(() => animations.forEach((animation: Animationable) => animation.removeAllListeners()));
sequence.push(() => animations.forEach((animation) => animation.removeListener('tick', onTick)));

// We can't allow Promise.all() here or anything that could render the shapes concurrently or parallel
// We can't allow Promise.all() here or anything that could render the shapes concurrently
// Hence, we need to reduce the sequence to the chain of promises, so we can get waterfall rendering
return await sequence.reduce(async (promise, item) => await promise.then(item), Promise.resolve());
}
Expand All @@ -141,12 +160,14 @@ export class Slide {
}

private initShapes (declaration: ShapeDeclaration[]): void {
declaration.forEach((shapeDeclaration: ShapeDeclaration) => {
declaration.forEach((shapeDeclaration) => {
const ctor = SHAPES.get(shapeDeclaration.type as ShapeType);

if (typeof ctor === 'undefined') {
throw new Error(
`Shape "${shapeDeclaration.name}" (${shapeDeclaration.type}) is unknown for me, maybe you made a typo?`
`You have specified a shape with the name "${shapeDeclaration.name}" in slide "${this.name}". ` +
`This shape has an unknown type "${shapeDeclaration.type}". ` +
'Maybe you made a typo in "type" or tried to use a shape we do not have implemented.'
);
}

Expand All @@ -155,13 +176,14 @@ export class Slide {
}

private initAnimations (declaration: AnimationDeclaration[]): void {
declaration.forEach((animationDeclaration: AnimationDeclaration) => {
declaration.forEach((animationDeclaration) => {
const ctor = ANIMATIONS.get(animationDeclaration.type as AnimationType);

if (typeof ctor === 'undefined') {
throw new Error(
`Animation "${animationDeclaration.name}" (${animationDeclaration.type}) is unknown for me, ` +
'maybe you made a typo?'
`You have specified an animation with the name "${animationDeclaration.name}" in slide "${this.name}". ` +
`This animation has an unknown type "${animationDeclaration.type}". ` +
'Maybe you made a typo in "type" or tried to use an animation we do not have implemented.'
);
}

Expand All @@ -171,7 +193,7 @@ export class Slide {

private renderShapes (shapes: ShapeRenderable[]): void {
this.cursor.eraseScreen();
shapes.forEach((shape: ShapeRenderable) => shape.render(this.cursor));
shapes.forEach((shape) => shape.render(this.cursor));
this.cursor.flush();
}
}

0 comments on commit c494c26

Please sign in to comment.