Testing custom pipes in Angular - javascript

I am trying to write some basic tests for my custom pipe, but being new to Jasmine and Angular pipes I am having some difficulty. Here is my pipe:
decimal-format-pipe.js
import { Pipe , PipeTransform } from '#angular/core';
import { DecimalPipe } from '#angular/common';
#Pipe({
name: 'myDecimalFormatingPipe'
})
export class MyDecimalFormatPipe implements PipeTransform {
constructor(public decimalPipe: DecimalPipe) {};
transform(value: any) {
if (value || value === 0) {
value = this.decimalPipe.transform(value, '1.2-2');
}
return value;
}
}
Obviously, this 'custom' pipe simply implements the decimal pipe transform() for now, but that will change in the future.
Here is my spec:
import { MyDecimalFormatPipe } from './my-decimal-format.pipe';
import { DecimalPipe } from '#angular/common';
describe('MyDecimalFormatPipe', () => {
let pipe: MyDecimalFormatPipe;
let decimalPipe: DecimalPipe;
let inputValue: any = '2.1111';
beforeEach( () => {
decimalPipe = new DecimalPipe(inputValue);
myPipe = new MyDecimalFormatPipe(decimalPipe);
});
it('pipe is defined', () => {
expect(pipe instanceof MyDecimalFormatPipe).toBeTruthy();
});
describe('transform ', () => {
it('should return correct value type ', () => {
spyOn(decimalPipe, 'transform').and.callThrough();
decimalPipe.transform(inputValue, '1.2-2');
expect(decimalPipe.transform).toEqual('2.11');
});
});
});
My first spec passes but for the transform() test it fails and I get the
error:
RangeError: Invalid language tag: 2.1111
at new NumberFormat (native)
at Function.NumberFormatter.format (
I cannot recall the last time I have seen this error. What does 'invalid language tag' refer to? What is it about this spec that is making it break?

Completing y_vyshnevska's answer, there are several points that are to be noted :
You need to use decimalPipe = new DecimalPipe('arab'); to tell the DecimalPipe constructor to use the arabic number format (in your case).
From the official documentation, I believe you don't need to use a spy for this test (https://angular.io/guide/testing#pipes), but simply getting the returned result from your pipe should be enough.
Edited parts :
beforeEach(() => {
decimalPipe = new DecimalPipe('arab');
pipe = new MyDecimalFormatPipe(decimalPipe);
});
...
it('should return correct value type ', () => {
// spyOn(pipe, 'transform').and.callThrough();
const result = pipe.transform(inputValue);
expect(result).toEqual('2.11');
});
...
N.B: One funny thing I could witness, is that with the Headless chrome the test would pass as the result is correct (2.11); but in my chrome browser the test would fail saying the result is incorrect (2,11). I guess this is due to the browser settings, but I didn't expect that either :-)

What does 'invalid language tag' refer to?
The DecimalPipe constructor has an injected string argument for locale, which is used for Intl.NumberFormat construction deeper. Try
new Intl.NumberFormat('1.222').format(1.2222) in browser console, you will see your error.

Related

Single instance modules in JavaScript (TypeScript)

I'm trying to define a simple assertion package in TypeScript so that assertions can be turned off during efficiency testing (and production eventually). Here's my attempt for a simple module:
let assertionsEnabled = true;
export function withAssertions<T>(val : boolean, body : () => T) : T {
const savedEnabled = assertionsEnabled;
try {
assertionsEnabled = val;
return body();
} finally {
assertionsEnabled = savedEnabled;
}
}
export function assert(condition : () => boolean, message ?: string) {
if (assertionsEnabled) {
if (!condition()) {
throw new Error("assertion failed" + (message ?? ""));
}
}
}
The problem is that both the Jest test and the class being tested each import this module and they seem to end up with two different assertionsEnabled variables. My test is encoded with
describe('building', () => {
withAssertions(false, () => {
...
});
});
and in the ADT, I have a bunch of assertions, but even though assertions are turned off in the Jest test, they are still on in the ADT. I thought modules were supposed to have a single instance. But obviously I'm doing something wrong.
I'm betting you have asynchronous stuff going on in your tests or stuff you instantiate as part of exporting from the module and it's happening before the jest stuff starts being executed.
Also, you might instead want to do a describeWithAssertion function instead, like so...
function describeWithAssertion(
ae: boolean,
title: string,
f: any // this should be specified better
) {
const prevAE = assertionsEnabled;
return describe(
title,
() => {
beforeEach(() => assertionsEnabled = ae);
afterEach(() => assertionsEnabled = prevAE);
f();
}
);
}
This way, even if f contains async functionality, jest is worrying about it, not you.

How to unit test form validation in Aurelia

I am trying to implement some unit tests on a form to see if the validation rules are working as expected.
from this page : https://github.com/aurelia/testing/issues/63
I found this implementation : https://github.com/aurelia/validation/blob/master/test/validate-binding-behavior.ts
and I tried to implement it in my project
login.spec.js
import {bootstrap} from 'aurelia-bootstrapper';
import {StageComponent} from 'aurelia-testing';
import {PLATFORM} from 'aurelia-pal';
import { configure, blur, change } from './shared';
import { Login } from './login';
describe('ValidateBindingBehavior', () => {
it('sets validateTrigger', (done) => {
const component = StageComponent
.withResources(PLATFORM.moduleName('features/account/login/login'))
.inView('<login></login>')
.boundTo({});
component.bootstrap(configure);
let viewModel;
const renderer = { render: jasmine.createSpy() };
component.create(bootstrap)
// grab some references.
.then(() => {
viewModel = component.viewModel;
viewModel.controller.addRenderer(renderer);
})
.then(() => expect(viewModel.controller.errors.length).toBe(0))
.then(() => blur(viewModel.firstName))
.then(() => expect(viewModel.controller.errors.length).toBe(1))
.then(() => component.dispose())
.then(done);
});
});
login.js
import { inject, NewInstance } from 'aurelia-dependency-injection';
import { ValidationController } from 'aurelia-validation';
import { User } from './login.model';
#inject(NewInstance.of(ValidationController), User)
export class Login {
constructor(controller, user) {
this.controller = controller;
this.firstName = '';
this.lastName = '';
this.userName = '';
this.showForm = true;
this.user = user;
}
};
login.model.js
import {ValidationRules} from 'aurelia-validation';
export class User {
firstName = '';
lastName = '';
userName = '';
constructor() {
ValidationRules
.ensure('firstName')
.required()
.ensure('lastName')
.required()
.minLength(10)
.ensure('userName')
.required()
.on(this);
}
}
shared.js
import {DOM, PLATFORM} from 'aurelia-pal';
export function configure(aurelia) {
return aurelia.use
.standardConfiguration()
.plugin(PLATFORM.moduleName('aurelia-validation'))
}
export function blur(element) {
element.dispatchEvent(DOM.createCustomEvent('blur', {}));
return new Promise(resolve => setTimeout(resolve));
}
export function change(element, value) {
element.value = value;
element.dispatchEvent(DOM.createCustomEvent('change', { bubbles: true }));
return new Promise(resolve => setTimeout(resolve));
}
and here is a piece of html markup :
<div>
<input ref="firstName" type="text" value.bind="user.firstName & validateOnBlur"
validation-errors.bind="firstNameErrors">
<label style="display: block;color:red" repeat.for="errorInfo of firstNameErrors">
${errorInfo.error.message}
</label>
</div>
<div>
in the spec, when I blur the element I expect to get one error, but "controller.errors" is always an empty array. and I get this for the failed message :
Error: Expected 0 to be 1.
UPDATE 1:
I tried to validate manually, so I added this in my spec :
.then(()=>
viewModel.controller.validate({object: viewModel.user, propertyName: 'firstName' })
)
and it works fine, but the blur and change functions don't trigger validation.
UPDATE 2:
I changed it like "Sayan Pal" suggested. and it works now but with a tiny problem. when I "blur" the element once it shows one error. but when I "blur" several elements ( let's say three ) it doesn't show the last error. in this case controller.errors.length would be 2.
I can blur the last element two times to get the correct length of errors. but I think there should be a better solution.
.then(() => blur(viewModel.firstName))
.then(() => blur(viewModel.userName))
.then(() => blur(viewModel.lastName))
.then(() => blur(viewModel.lastName))
I think instead of using createCustomEvent you simply need to do element.dispatchEvent(new Event("blur"));. Same goes for change event.
This has always worked for me, and hope it will help you too :)
On related note, I use a default ValidationController generator factory method that ensures the default trigger as follows.
import { validateTrigger, ValidationControllerFactory } from "aurelia-validation";
...
const validationController = validationControllerFactory.createForCurrentScope();
validationController.changeTrigger(validateTrigger.changeOrBlur);
Update after OP updated the question
It is difficult to say why it is happening, without debugging. As I don't see any imminent problem in your test code, my assumption is that it is a timing issue. The main idea is that you need to wait for the change to happen. There are several ways you can do it, all of those needs change in how you are asserting.
One way to do it is to employ a promise with a timeout that polls in a regular interval for the change. And then wait for the promise.
Or you can use TaskQueue to queue your assertion, and after the assertion call done. This looks something like below.
new TaskQueue().queueMicroTask(() => {
expect(foo).toBe(bar);
done();
});
Other alternative is to use cypress as an e2e test framework. Out of the box, Cypress waits for the change to happen until times out.
Choose what best fits your need.

Using bracket notation depending on input data

I'm in the process of optimizing some code in my library, however, I have a bit of an issue regarding why bracket notation isn't working when trying to call an imported class.
Parameter type accepts a string that is camelCased, such as: myString.
The parameter data can be anything.
import { foo } from './example';
export const find = (type: string, data: any) => {
// This next line effectively deletes the end of the string starting
// from the first capital letter.
const f = type.replace(/[A-Z][a-z]+/, '');
try {
return [f][type](data);
} catch (e) {
return e;
}
};
this is what I expect it to look like if I was to visualize it using dot notation:
foo.fooBar(someRandomData)
This should call the static method fooBar(data) on the imported class foo,
however, I receive an error message:
TypeError: [f][type] is not a function
If I was to revert it back to my if..else if style, it works:
if (type.startsWith('foo')) return foo[type](data);
How can I do what is desired above without getting the defined error message?
Thank you for your help in advance!
EDIT: This is an example I modified from already existing code, therefore, I fixed a few typos.
EDIT #2: as per requested, the imported class foo looks like this:
export class foo{
static fooBar(data){
// Do stuff with data...
}
In the end you need some reference to the classes or object to get started with. Here is a working example of how you could do this type of functionality, but you have start with a map of your class instances so you can get to them:
class foo {
fooBar(data: any) { return { name: 'foo', data } };
}
class example {
exampleFood(data: any) { return { name: 'example', data } };
}
var lookup: { [classes: string]: any; } = { };
lookup['foo'] = new foo();
lookup['example'] = new example();
const find = (encodedType: string, data: any) => {
// This next line effectively deletes the end of the string starting
// from the first capital letter.
const f = encodedType.replace(/[A-Z][a-z]+/, '');
try {
return lookup[f][encodedType](data);
} catch (e) {
return e;
}
};
alert(JSON.stringify(find("fooBar", "Found you foo")));
alert(JSON.stringify(find("exampleFood", "Found you example")));
I would suggest you instead move over to using the nodeJS built-in EventEmitter.
You can do something like:
import * as EventEmitter from 'events';
import { foo } from './example';
import { bar } from './example2';
export const discordEventEmitter = new EventEmitter();
discordEventEmitter.on('fooBar', foo.fooBar);
discordEventEmitter.on('fooToo', foo.fooToo);
discordEventEmitter.on('barBell', bar.barBell);
Then, when you want to fire an event, you can simply:
discordEventEmitter.emit('fooBar', someData);
You can also simplify the event handler registration by writing:
const fooProps = Object.getOwnPropertyNames(foo) as (keyof typeof foo)[];
fooProps.filter(k => typeof foo[k] === 'function').forEach(funcName => {
discordEventEmitter.on(funcName, foo[funcName]);
});
const barProps = Object.getOwnPropertyNames(bar) as (keyof typeof bar)[];
fooProps.filter(k => typeof bar[k] === 'function').forEach(funcName => {
discordEventEmitter.on(funcName, bar[funcName]);
});

how do I make this class reuseable in typescript? meta programming? subclasses? injected module?

I've got this EventsStorage typescript class that is responsible for storing and retrieving Event objects in ionic-storage (wrapper for sqlite and indexedDB). It uses my Event class throughout.
I would like to reuse a lot of this logic for something other than an Event, like a Widget.
I come from a ruby background where it would be relatively simple to extract all the storage logic, set a ruby var that is literally the class Event and use that var wherever I use Event. Can I do something similar in typescript? Is there another mechanic I can use to reuse the bulk of this class for something else, like Widget?
Ideally, my EventsStorage class becomes really lightweight, and I'm not just wrapping calls to this.some_storage_module.get_ids() or this.some_storage_module.insert_new_objs() -- which would have to be copy/pasted to every other instance I needed this.
Something like this:
export class EventsStorage { // extends BaseStorage (maybe??)
constructor(){
super(Event, 'events'); // or some small set of magical args
}
}
Here's the existing class:
import { Injectable } from '#angular/core';
import { Storage } from '#ionic/storage';
import { Event } from '../classes/event';
// EventsStorage < EntityStorage
// - tracks local storage info
// - a key to an array of saved objects
// - a query() method that returns saved objects
#Injectable()
export class EventsStorage {
base_key: string;
ids_key: string;
constructor(
private storage: Storage
){
this.base_key = 'event';
this.ids_key = [this.base_key, 'ids'].join('_');
}
get_ids(): Promise<any>{
return this.storage.ready().then(() => {
return this.storage.get(this.ids_key).then((val) => {
if(val === null){
return [];
} else {
return val;
}
});
});
}
insert_new_objs(new_objs: any): Promise<any>{
return new_objs.reduce((prev: Promise<string>, cur: any): Promise<any> => {
return prev.then(() => {
return this.storage.set(cur._id, cur.event);
});
}, Promise.resolve()).then(() => {
console.log('saving event_ids');
return this.storage.set(this.ids_key, new_objs.map(obj => obj._id));
});
}
update(events: Event[]): Promise<any> {
let new_objs = events.map((event) => {
return {
_id: [this.base_key, event.id].join('_'),
event: event
};
});
return this.insert_new_objs(new_objs);
}
query(): Promise<Event[]>{
let events = [];
return this.get_ids().then((ids) => {
return ids.reduce((prev: Promise<string>, cur: string): Promise<any> => {
return prev.then(() => {
return this.get_id(cur).then((raw_event) => {
events = events.concat([raw_event as Event]);
return events;
});
});
}, Promise.resolve());
});
}
get_id(id: string): Promise<Event>{
return this.storage.get(id).then((raw_event) => {
return raw_event;
});
}
}
It looks to me like you want to use generics. You basically define some basic interface between all the things you'll want to store, and your code should depend on that interface. In your code as far as I can tell you only use the id property.
So it would look kinda like this
import { Event } from '...';
import { Widget } from '...';
interface HasId{
id: string;
}
class ItemsStorage<T extends HasId> {
....
get_id(id: string): Promise<T>{
...
}
}
const EventStorage = new ItemsStorage<Events>(storage);
const WidgetStorage = new ItemsStorage<Widget>(storage);
const ev = EventStorage.get_id('abc'); //type is Promise<Event>
const wd = WidgetStorage.get_id('def'); //type is Promise<Widget>
You can read more about generics here.
Edit:
1 - about subclassing - It's usually less preferable. If your ItemsStorage class need different behavior when dealing with Events vs Widgets, than subclassing is your solution. But if you have the same behavior for every class, one might call your code generic, and using generics is better.

Access to class scope inside lambda in TypeScript is undefined?

I have the following class in TypeScript that is used as a Pipe by Angular 2 to render markdown. It compiles without errors, but hits an exception at runtime on the marked line:
var Remarkable = require('remarkable');
#Pipe({
name: 'markdown'
})
export class MarkdownPipe implements PipeTransform {
public remarkable;
constructor(private sanitizer: DomSanitizationService) {
this.remarkable = new Remarkable({
typographer: true
});
this.remarkable.use(this.parseVideos);
}
transform(value: string) : SafeHtml {
if (value != null) {
return this.sanitizer.bypassSecurityTrustHtml(this.remarkable.render(value));
}
return null;
}
public parseVideos(md) : void {
md.inline.ruler.push('video', (state) => {
return this.parseMedia(state, '#', 'video'); // this is undefined
});
}
public parseMedia(state, startingMarker, tokenName) : boolean {
// do stuff
}
}
When I this code attempts to execute, I get a runtime error telling me that:
Where _this refers to the same line I have commented above. Why is this? My IDE reports that I should have access to the parseMedia method inside of my lambda expression.
What's the best solution to this?
That's because you pass it here:
this.remarkable.use(this.parseVideos);
Then when the method is invoked the this no longer points to your instance of MarkdownPipe.
In order to preserve the right scope for this you can either have another arrow function:
this.remarkable.use(md => this.parseVideos(md));
Or you can use the Function.prototype.bind():
this.remarkable.use(this.parseVideos.bind(this));

Categories