A night out with Jasmine

A night out with Jasmine

Unit testing is much more than a tool for ensuring that a program behaves as expected. Nowadays it's the key for new development techniques such as TDD and BDD in which you begin writing tests before the required code. It's all about requirements and behaviour; developers must focus on understanding the requirements instead of thinking about the way they implement the code. In the event that you've forgotten the fundamental benefits of unit testing here you have a quick reminder:

  • Obviously, you are testing that your code works
  • When changes are made you can automatically verify that everything works
  • Tests are a pretty good way to document the project.

Jasmine is a BDD framework for javascript code. In a nutshell, BDD it's an evolution of TDD where unit tests look like user stories, and so tests are defined in the same terms of the expected behaviour of the software units. When you discover functions names in Jasmine tests these simple definitions will make more sense.

Let's get our hands dirty, while you take a look at Jasmine's site http://jasmine.github.io/2.0/introduction.html you can get the master from the github site https://github.com/pivotal/jasmine/archive/master.zip

Or, if you are using node then just sigh, coff slightly, adjust your spectacles and simply try with this: npm -g install jasmine-node

Beware! at the time of writing this Jasmine is in 2.0 version and there are some changes and deprecated functions. So, be careful with what you read out there, check dates.

Ok, once jasmine is installed let's write this piece of code in a file called hello.js

exports.hello = function () {
	return 'Hello world';
};

Behold the almighty helloworld sample.

With Jasmine, the test file is expected to live in a dir called specs in a file called name.spec.js, so hello.spec.js in this case.

And this is what the test would look like:

/*
* specs/hello.spec.js
*/
var hl = require("../hello.js");

// testing hello function
describe("hello", function() {
	it('returns "Hello world"', function() {
		expect(hl.hello()).toEqual("Hello world");
	});
});

To run the test, in the same directory where hello.js resides we type jasmine-node command with the dir of the test and it will pass all specs files within


linux$ jasmine-node specs/
.

Finished in 0.020 seconds
1 tests, 1 assertions, 0 failures, 0 skipped
linux$ 

¿How could we test functions that are using a callback? In the same hello.js file we add this function:


exports.helloAsync = function (callback) {
	return callback(null,'Async Hello World');
};

And this is how the test should look like:

describe("helloAsync", function() {
	it("does an asynchronous call", function() {
		hl.helloAsync(function(err,response) {
			expect(response).toContain("Async Hello World");
		});
	});
});

Now if we run the test, we will check 2 test and 2 assertions:

linux$ jasmine-node specs/
..

Finished in 0.029 seconds
2 tests, 2 assertions, 0 failures, 0 skipped
linux$ 

Maybe you've noticed that in the second test instead of toEqual we're using .toContain. These are different matchers with different effects as indicated by their obvious names. As you can see, Jasmine drives us to write tests that apart from being 'extracool' can be read as natural language.

Developing a simply utility, TDD style

Our beloved system administrator wants a program to convert people names such as "john, doe" into account names like "john_doe". Remember, we are trying TDD now so focus on the requirement, and try not to think about using regular expressions or any kind of implementation details. TDD dictates to follow this steps:

  1. Write the test code
  2. Pass the test and see how it fails
  3. Write just the necessary code, quick and dirty, to turn the test from red to green
  4. Once the test is passed successfuly, refactor the code.
  5. Feel kwel, you earned a good cup of whatever

Ok, we create a specs dir, and inside a username.spec.js file containing our test of a code that right now it is but a ghost:


var username = require('../username.js');

describe('Test to ensure username is created', function () {
	it('returns name_surname with simple Name,Surname',function () {
		expect(username.create('john,doe')).toEqual('john_doe');
	});

});

Not only if we test this fails, but also if we run it this code FAILS. why? username.js does not exist, as simply as that. Well then, time for step 2 in TDD, we write our code in a file named username.js one dir level below.

/**
* username.js
* Pello Altadill Izura
* http://pello.info
*/

exports.create = function (personName) {
	var result = '';
	var parts = personName.split(',');
	result = parts[0] + '_' + parts[1];

	return result;
};

Now what we get is what we expected to:

linux$ jasmine-node specs/
.

Finished in 0.033 seconds
1 test, 1 assertion, 0 failures, 0 skipped

linux$

The requirements were minimal in order to show the process of TDD. But the world is not a pleasant place where everything just works fine. As Melissandre said, there is one hell, the one we are living in now. This could be a more realistic problem, which might be more useful to ilustrate some other Jasmine features.

Our Benevolent Dictator thinks that we deserve the honor to raise from the mud in which we creep and serve him with our bare hands, although we are not worthy of such a gift from a demigod. We want to get on well with OBD if we want him to lower the proxy constraints for us among other mundane privileges. He is in a need for a program that translates people names to system names with this specifications:

  • People names come with Name, Surname notation and they may be translated to name_surname
  • Names can come in either lower or uppercase
  • Names can come with extra spaces
  • Some people may have two names like 'John John, Kennedy' or 'Jar Jar, Binks' and they might be translated to name1name2_surname
  • Some names can be null or empty spaces ','. (That may be OBD's fault but It's better not to mention it)

We'll try to acomplish these specs, so our unit tests might look like this in Jasmine style:


var username = require('../username.js');

describe('Test to ensure username is created', function () {
	it('returns name_surname with simple Name,Surname',function () {
		expect(username.create('john,doe')).toEqual('john_doe');
	});

	it('converts any uppercase character to lowercase', function () {
		expect(username.create('Han,Solo')).toEqual('han_solo');
	});

	it('removes any space character', function () {
		expect(username.create('   Luke, Skywalker ')).toEqual('luke_skywalker');
	});

	it('returns multiple names together', function () {
		expect(username.create('Obi Wan,Kenobi')).toEqual('obiwan_kenobi');
	});

	it('throws error if name is empty', function () {
		expect(function() { 
					username.create(''); 
			   }).toThrow('Person name is empty');
	});
});

If we test all of this with our previous code Jasmine will scream our name:

linux$ jasmine-node specs/
.FFFF

Failures:

  1) Test to ensure username is created converts any uppercase character to lowercase
   Message:
     Expected 'Han_Solo' to equal 'han_solo'.
   Stacktrace:
     Error: Expected 'Han_Solo' to equal 'han_solo'.
    at null. (/root/nodejs/jasmine/users/specs/username.spec.js:9:39)

  2) Test to ensure username is created removes any space character
   Message:
     Expected '   Luke_ Skywalker ' to equal 'luke_skywalker'.
   Stacktrace:
     Error: Expected '   Luke_ Skywalker ' to equal 'luke_skywalker'.
    at null. (/root/nodejs/jasmine/users/specs/username.spec.js:13:50)

  3) Test to ensure username is created returns multiple names together
   Message:
     Expected 'Obi Wan_Kenobi' to equal 'obiwan_kenobi'.
   Stacktrace:
     Error: Expected 'Obi Wan_Kenobi' to equal 'obiwan_kenobi'.
    at null. (/root/nodejs/jasmine/users/specs/username.spec.js:17:45)

  4) Test to ensure username is created returns empty string with spaces
   Message:
     Expected '_undefined' to equal ''.
   Stacktrace:
     Error: Expected '_undefined' to equal ''.
    at null. (/root/nodejs/jasmine/users/specs/username.spec.js:21:31)

Finished in 0.061 seconds
5 tests, 5 assertions, 4 failures, 0 skipped

Let's fix each of the tests


/**
* username.js
* Pello Altadill Izura
* http://pello.info
*/

exports.create = function (personName) {
	var result = '';
	personName = personName.toLowerCase();
	personName = personName.replace(/\s/g,'');
	if (personName == '' ) {
		throw new Error('Person name is empty');
	}
	var parts = personName.split(',');
	result = parts[0] + '_' + parts[1];

	return result;
};

The \s replacement fixes many things. And now we throw an Error whenever a personName comes empty. Maybe this is not the preferred behaviour of our program, but It helps to show how to check if exceptions are thrown.

Jasmine matchers at a glance

We have already used .toEqual(), .toContain() and .toThrow(), but if you're familiar with unit testing you might expect many others to exist. So instead of blowing your mind with poor-man's wordgames, let's see what we have inside Jasmine's toolbox.

Negate matching

at any moment we can reverse the matcher using a not like this

expect(666).not.toEqual(333);
toEqual

Tests equality with primitive types: numbers, strings, booleans, null. But NOT with objects. Some examples:

		var one = {'name' : 'John'};
		var other = {'name' : 'John'};
		var another = one;
		var oneArray = [42, 15, 69];
		var otherArray = [42, 15, 69];
		var anotherArray = [6, 66, 666];
		var phrase = 'By demon be driven';


		expect(6).toEqual(6);
		expect(6).toEqual(6.0);
		expect(one).toEqual(other);
		expect(oneArray).toEqual(otherArray);
		expect(6).not.toEqual('6');
toBe

Tests equality between objects. Some examples:

		var one = {'name' : 'John'};
		var other = {'name' : 'John'};
		var another = one;
		var oneArray = [42, 15, 69];
		var otherArray = [42, 15, 69];
		var anotherArray = [6, 66, 666];

		expect(6).toBe(6);
		expect(6).not.toBe('6');
		expect(6).toBe(6.0);
		expect(one).toBe(another);
		expect(oneArray).not.toBe(otherArray);
		expect(one).not.toBe(other);
toContain

Checks if an element of a string or of an array is present. Some examples:

		var phrase = 'By demon be driven';

		expect(oneArray).toContain(42);
		expect(phrase).toContain('demon');
toBeTruthy

Checks if what we expect is true. Some examples:

		expect(true).toBeTruthy();
		expect(1).toBeTruthy();
		expect('Metallica').toBeTruthy();
		expect(66).toBeTruthy(); // anything but 0
toBeFalsy

Checks if what we expect is false. Some examples:

		var phrase = 'By demon be driven';

		expect(false).toBeFalsy();
		expect(0).toBeFalsy();
		expect('').toBeFalsy();
		expect(null).toBeFalsy();
		expect(undefined).toBeFalsy();
		expect(NaN).toBeFalsy();
		expect(true).not.toBeFalsy();
		expect(phrase).not.toBeFalsy();
toBeNull

Checks if something is null. Some examples:

		expect(null).toBeNull();
		expect('').not.toBeNull();
		expect([]).not.toBeNull();
toBeNaN

Checks if we have anything but a number. Some examples:

		var phrase = 'By demon be driven';

		expect(NaN).toBeNaN();
		expect(parseInt('One')).toBeNaN();
		expect(parseInt('666')).not.toBeNaN();
		expect(phrase).not.toBeNaN();
		expect(6).not.toBeNaN();
toBeDefined

Checks if a variable is defined. Some examples:

		var unnamed;
		var god = 'Cthulhu';
		var anotherGod = god;
		expect(god).toBeDefined();
		expect(another).toBeDefined();
		expect(666).toBeDefined();
		expect(true).toBeDefined();
		expect(null).toBeDefined(); // yes, it is
		expect(unnamed).not.toBeDefined();
toBeUndefined

Checks if a variable is not defined Some examples:

		var unnamable;
		var unnamed;
		expect(unnamable).toBeUndefined();
		expect(unnamed).toBeUndefined();
		expect(null).not.toBeUndefined();
		// This would throw Reference Error
		//expect(notDeclaredVar).toBeUndefined();
toBeGreaterThan

Checks if the value, integer or String is greater than other. Some examples:

		expect(6).toBeGreaterThan(5);
		expect('b').toBeGreaterThan('a');
		expect(6).not.toBeGreaterThan('6');
toBeLessThan

Checks if the value is lesser. Some examples:

		expect(5).toBeLessThan(6);
		expect(-5.1).toBeLessThan(-5.0);
		expect('a').toBeLessThan('b');
toBeCloseTo

Checks if a value is closed to another providing certain precision. When the precision is 0 it parses the number to Int. Some examples:

		expect(66.64).toBeCloseTo(66.62,1);
		expect(66.64).toBeCloseTo(67,0);
		expect(42.44).toBeCloseTo(42,0);
		expect(42.59).toBeCloseTo(43,0);
		expect(Math.PI).toBeCloseTo(3.14,2);
		expect(Math.PI).toBeCloseTo(3,0);
toMatch

This is great, we can check if the value matches a regular expression. Some examples:

		expect('Thorin Kili Gloin').toMatch(/gloin/i);
		expect('Jasmine').toMatch(/[a-z]+/i);
		expect('Jasmine and Eugene').not.toMatch(/^[a-z]+$/gi);
		expect('Jasmine and Eugene').toMatch(/^[a-z\s]+$/gi);
toThrow

As explained before, checks if an Error is thrown.

We'll try with this dumb function:
		var vomitError = function () {
			throw new Error('I puke errors');
		};

	it('checks toThrow',function () {
		expect(vomitError).toThrow();
	});
Creating our own matchers

Maybe you need a more specific matcher to be able to test something that Jasmine's default matchers are not capable of. Well, you can define your own matchers. Inside the describe block we can define our own matcher, in this case is one matcher to test if a String is complex enough to be a secure password.

	/**
	* customMatcher
	* toBeComplex
	* checks if a String contains alphanumeric and symbols
	* and a min lenght of 8
	*/
	beforeEach(function() {
		this.addMatchers({
			toBeComplex: function() {
				this.message = function() {
					return "Expected " + this.actual + " to be complex";
				};
					return this.actual.match(/[a-z]{1,}/) 
						&& this.actual.match(/[A-Z]{1,}/)
						&& this.actual.match(/[0-9]{1,}/)
						&& this.actual.match(/[\.\,\;\:\-\!\?\_]{1,}/)
						&& (this.actual.length>7);
			}
		});

	});

And now we can use our tester just like the others:

	// toBeComplex
	// Checks if a String is valid to be a password
	it('checks toBeComplex',function () {
		expect('Death-666').toBeComplex();
		expect('.-;.:,!?').not.toBeComplex();
		expect('Death66').not.toBeComplex();
		expect('josua').not.toBeComplex();
	});

'josua' has always been a very bad password.

If you need params in this matcher is quite easy to add them. We define a slightly different matcher now adding the minimun length.

			toBeComplexAndLongerThan: function(length) {
				this.message = function() {
					return "Expected " + this.actual + " to be complex and longer than " + length;
				};
					return this.actual.match(/[a-z]{1,}/) 
						&& this.actual.match(/[A-Z]{1,}/)
						&& this.actual.match(/[0-9]{1,}/)
						&& this.actual.match(/[\.\,\;\:\-\!\?\_]{1,}/)
						&& (this.actual.length>length);
			}

Now we test it:

		expect('Josua:It666').toBeComplexAndLongerThan(10);
Spies, spies everywhere

Matches are fine to check if our functions are returning what are expected to. But, how could we be sure that certain functions are being called? Jasmine as many other tools brings us the possibility to introuce the so called spies. By the way I introduce the use of setup/teardown like methods in Jasmine: beforeEach and afterEach.

First I'll show how to use the spies in a very simple problem, and after that we'll move to test a tcp server with async calls.

stringutils: this is a simple class with some methods to do simple stuff with strings.

/**
* stringutils.js
* Some String utils 
*/
exports.StringUtils = function () {

		this.len = function (string) {
			return string.length;
		};

		this.vowels = function (string) {
			var totalVowels = 0;

			for (var i = 0; i< this.len(string);i++) {
				if (string[i].match(/^[aeiou]{1}$/i)) {
					totalVowels++;
				}
			}

			return totalVowels;
		};

		this.reverse = function (string) {
			var gnirts = '';
			for (var i = this.len(string) -1;i>=0 ;i--) {
				gnirts += string[i];
			}

			return gnirts;
		}
};

Ok, I'm afraid that I'll not get rich with this, but should be enough to apply simple spies on it in this spec below:

var stringutils = require("../stringutils.js");

// testing hello function
describe("testing stringutils", function() {

	var myStringUtils;

	beforeEach(function() {
		myStringUtils = new stringutils.StringUtils();			
		spyOn(myStringUtils,'len');
		spyOn(myStringUtils,'vowels');
		spyOn(myStringUtils,'reverse');

		myStringUtils.len('Eugene');
		myStringUtils.vowels('Eugene');
		myStringUtils.reverse('Eugene');
    });

	it('calls len function',function () {
		expect(myStringUtils.len).toHaveBeenCalled();
		expect(myStringUtils.len).toHaveBeenCalledWith('Eugene');
	});

	it('calls vowels function',function () {
		expect(myStringUtils.vowels).toHaveBeenCalled();
		expect(myStringUtils.vowels).toHaveBeenCalledWith('Eugene');
	});


	it('calls reverse function',function () {
		expect(myStringUtils.reverse).toHaveBeenCalled();
		expect(myStringUtils.reverse).toHaveBeenCalledWith('Eugene');
	});

	afterEach(function () {

	});

});

To set spies, we have to call spyOn method. Then we can check if any method has been called and even we can test if they where called with certain parameters.

  • toHaveBeenCalled()
  • toHaveBeenCalledWith(something)

We could chain some of these to make the function behave as we want:

  • andCallFake: instead of call
  • andCallThrough(): calls the original functions that spyOn is faking
  • andReturn(arguments): show as the parameters when the spied function is called
  • andThrows(exception): throws an exception when the function is called
Another sample, a tcp server

Now let's try harder with a simple node tcp echo server. This is the server

/**
* echoserver.js
* Simple sample echo server writen in nodejs
* Pello Altadill
*/
var net = require('net');

exports.EchoServer = function (port, host) {

	var self = this;
	this.port = port || 1666;
	this.host = host || '0.0.0.0';
	this.server = net.createServer();
	this.timeout = 5000; // 5 secs
	this.keepAlive = true;
	this.keepAliveTime = this.timeout;
	this.clientSockets = [];

	this.init = function () {
		this.server.listen(this.port, this.host);
	};

	this.listening = function() {
		console.log('Server> Server listening on port ' + port);
	};

	this.connection = function(socket) {
		self.clientSockets.push(socket);
		console.log('Server> Connected to server, total clients: ' + self.clientSockets.length);
		
		// console.log(socket);
		if (this.keepAlive) {
			socket.setKeepAlive(this.keepAliveTime);
		} else {
			socket.setTimeout(this.timeout);
		}

		socket.on('timeout', function () {
			console.log('Server> timeout');
			socket.end();
		});

		socket.on('data', function (msg) {
			console.log('Server> Received data ' + msg);
			console.log('Server> Sending it back');
			socket.write(msg);
		});

		socket.on('close', function () {
			console.log('Server> Socket closed');
			var index = self.clientSockets.indexOf(socket);
			self.clientSockets.splice(index, 1);
		});
	};

	this.close = function(err) {
		console.log('Server> Connection closed ' + err);
	};

	this.error = function(err) {
		console.log('Server> Error on server ' + err);
	};
	
	this.close = function () {
		this.server.close();
		this.server.unref();
	};
	
	this.server.on('listening',this.listening);

	this.server.on('connection', this.connection); 

	this.server.on('close', this.close);

	this.server.on('error', this.error);

};

And this is a way to test some behaviours of the server

var server = require("../echoserver.js");
var net = require('net');
var myServer;

// testing hello function
describe("server", function() {

	var successCallBack;

	 beforeEach(function() {
  	
		successCallBack = jasmine.createSpy();

		myServer = new server.EchoServer(1666,'0.0.0.0');
		spyOn(myServer, 'init').andCallThrough();
		spyOn(myServer.server, 'listen').andCallThrough();
		spyOn(myServer, 'connection').andCallThrough();
		myServer.init();
			
    });

	it('Check server port', function() {
		expect(myServer.port).toEqual(1666);
	});

	it('Check server host', function() {
		expect(myServer.host).toEqual('0.0.0.0');
	});

	it('Check server init was called', function() {
		expect(myServer.init).toHaveBeenCalled();
	});

	it('Check server listen was called', function() {
		expect(myServer.server.listen).toHaveBeenCalled();
	});
	
	it('Check server connection was called', function() {
		var client = net.connect({ port: 1666 },
            	function() {
               		client.write('Eat this madafaka!');
            	}, successCallBack);

		waitsFor(function() {
			console.log('Waiting here: ' + successCallBack.callCount );
			return successCallBack.callCount > 0;
		}, "operation never completed", 10000);

		runs(function() {
			expect(successCallBack).toHaveBeenCalled();
		});

	});

	it('checks that server responds the same ', function(done) {
			var client = net.connect({ port: 1666 });
			client.write('hello there');
	 		client.on('data', function(data){
	 			console.log(data.toString());
    			expect(data.toString()).toBe('hello there');
    			done();
  			});

	});

	 afterEach(function() {
    });
});

The last test is testing the result of an asynchronous call and it needs to use done() callback. This is valid for Jasmine >= 2.0 versions now. Maybe you've read something about run/waitFor but now this seems to be the official way to do this.

Some references:
  • Jasmine introduction: http://jasmine.github.io/2.0/introduction.html
  • Jasmine wiki: https://github.com/pivotal/jasmine/wiki/Spies
  • Intro about Jasmine, web focused
  • Javascript Unit Testing, by Hazem Saleh PACKT
  • Javascript Testing with Jasmine, by OREILLY

by Pello Altadill 1 Day ago - 24 hits

Arkapong

This a classic pong-like game that takes some ideas from Arkanoid. Basically two paddles try to hit the ball until one of them fails but, from time to time the rules can change if special bricks are touched. Red: invert ball direction White: triple ball Yellow: smaller paddles Blue: bigger paddles Green: speed up ball Orange: change...

by Pello Altadill 04/07/2014 07:35:33 - 207 hits

More »

Getting started with Node

Few days ago, a very dear friend of mine came up to my place with a present. It was a nodejs book. This is a new or maybe it would be more accurate to say an emergent technology which brings back an old idea that I used to hear of long time ago: a server side javascript. That was something that was supposed to exist somewhere, as it was mentioned in the prefaces of many javascript related papers: ...

by Pello Altadill 03/17/2014 23:48:44 - 271 hits

More »

Calcular el IBAN con php

Los organismos internaciones se han vuelto a poner de acuerdo para poner unificar los números de cuenta y gracias a ello todos los programadores y DBAs del mundo ya estamos enfrascados en la tarea de adaptar programas, validaciones, campos de las BBDD y un largo etcétera. En cada país el código de cuenta corriente puede variar y a través del IBAN se intenta unificar la forma de esa cuenta de tal forma que sea validable. ...

by Pello Altadill 01/18/2014 00:01:21 - 486 hits

More »

MessageQueue con Spring

Hace unos meses salió aquí un ejemplo de ActiveMQ sin Spring ni nada, aunque lo relevante era mostrar la configuración y uso del servidor de mensajes. Vamos ahora a hacer exactamente lo mismo pero utilizando Spring. Cosa que tenía pendiente desde entonces. Al igual que suce...

by Pello Altadill 01/06/2014 00:08:52 - 493 hits

More »

Acceder a web desde Android con java.net

Accceso a Web desde Android Las aplicaciones para móviles se podrían dividir en tres grandes grupos: las que se ejecutan totalmente de forma local, las que tienen toda la información en la web y las que combinan las dos cosas. Si tenemos que acceder a la web desde Android y teniendo en cuenta que lo hacemos con java tenemos dos opciones por defecto: Usar las librerías básicas de java.net Usar HttpClient HttpClient la librería de Apache ...

by Pello Altadill 11/23/2013 10:36:35 - 667 hits

More »

Maneras de integrar Struts2 con Hibernate

Struts2 sigue siendo uno de los frameworks web para java más populares. Lo del número 2 no está de más ya que ciertamente Struts2 y Struts a secas no tienen mucho que ver en cuanto a código. Aunque sí, formalmente los dos son frameworks MVC. Pero con Struts2 no es suficiente, ¿qué pasa con el acceso a los datos? Para facilitar la gestión de datos y ante todo poder centrarnos en el negocio y no andar por ejemplo cargando listas de objetos a mano de un resultset de jdbc disponemos de Hibernate, u...

by Pello Altadill 11/17/2013 15:26:23 - 681 hits

More »

Arreglar e Iniciar el Oracle Enterprise Manager

Hablar de Oracle trae gratos recuerdos como la saga de IronMan y en especial la tercera entrega donde me transportaron a la nube de Oracle. El Enterprise Manager es un interfaz web con el que podemos gestionar la BD de Oracle: inicio/parada/blackout, ficheros, memoria, tareas, backups, usuarios, roles, y en definitiva todos los objetos que forman parte de los...

by Pello Altadill 11/10/2013 01:01:04 - 671 hits

More »

Errores frecuentes en aplicaciones Struts2 con Eclipse y Tomcat

Introducción ¿Tu aplicación struts2 falla? ¿Ni si quiera arranca el tomcat o te está vomitando toda la stacktrace, toda la pila de llamadas erróneas por la consola? Antes de echarse a llorar, desesperarse, increpar al proyecto apache, al profesor debes asegurarte de unos mínimos: Tu proyecto no tiene errores de compilación Tus clases y jsps no tienen ni u...

by Pello Altadill 11/01/2013 01:44:50 - 764 hits

More »