Fixed paging bug in second frontend
diff --git a/Changes b/Changes
index 6411d84..ed744a2 100755
--- a/Changes
+++ b/Changes
@@ -1,8 +1,15 @@
-0.09 2014-12-21
-        - Added new API module
+0.11 2014-01-16
+        - Fixed paging bug with the async api.
+        - Disabled Testing feature in tutorial.
+	- Improved test suite.
+
+0.10 2014-12-21
 	- Fixed time_exceeded bug
 	- Fixed test_port bug for inspection button
 
+0.09 2014-12-03
+        - Added new API module
+
 0.08 2014-11-19
         - Added new suggestion module
 
diff --git a/lib/Korap.pm b/lib/Korap.pm
index 3702878..931fc2f 100644
--- a/lib/Korap.pm
+++ b/lib/Korap.pm
@@ -2,7 +2,7 @@
 use Mojo::Base 'Mojolicious';
 use Mojo::ByteStream 'b';
 
-our $VERSION = '0.08';
+our $VERSION = '0.11';
 
 # Start dev with
 # morbo -w lib -w templates -w public/sass -w public/js -w public/css script/korap
diff --git a/lib/Korap/API.pm b/lib/Korap/API.pm
index 01bda48..9392f4d 100644
--- a/lib/Korap/API.pm
+++ b/lib/Korap/API.pm
@@ -98,7 +98,8 @@
   # Search blocking
   else {
     my $tx = $ua->get($url);
-    return $self->_process_response('matches', $index, $tx);
+    $self->_process_response('matches', $index, $tx);
+    return $index;
   };
 };
 
diff --git a/lib/Korap/Controller/Search.pm b/lib/Korap/Controller/Search.pm
index 6f1fb74..a4ba64b 100644
--- a/lib/Korap/Controller/Search.pm
+++ b/lib/Korap/Controller/Search.pm
@@ -39,7 +39,7 @@
       $c->search(
 	cutoff => scalar $c->param('cutoff'),
 	count => scalar $c->param('count'),
-	start_page => scalar $c->param('page'),
+	start_page => scalar $c->param('p'),
 	cb => $delay->begin,
 	%param
       ) if $query;
diff --git a/lib/Korap/Plugin/KorapTagHelpers.pm b/lib/Korap/Plugin/KorapTagHelpers.pm
index 2c1cd6c..6753ad5 100644
--- a/lib/Korap/Plugin/KorapTagHelpers.pm
+++ b/lib/Korap/Plugin/KorapTagHelpers.pm
@@ -1,5 +1,6 @@
 package Korap::Plugin::KorapTagHelpers;
 use Mojo::Base 'Mojolicious::Plugin';
+use Mojo::JSON 'decode_json';
 use Mojo::JSON::Pointer;
 use Mojo::ByteStream 'b';
 use Mojo::Util qw/xml_escape/;
@@ -25,86 +26,91 @@
       }
 
       # Tutorial wasn't embedded - but opened for testing
-      elsif ($c->param('testing') &&
-	       $c->korap_test_port &&
-		 $param{tests}) {
+ #     elsif ($c->param('testing') &&
+#	       $c->korap_test_port &&
+#		 $param{tests}) {
+#
+# Currently disabled
 
-	my $tests = $param{tests} // [];
-	my $json = $c->search(
-	  query => $q,
-	  ql => $ql,
-	  cutoff => 'true',
-	  no_cache => 1
-	);
-
-	# There is a response
-	if ($json) {
-	  my $json_pointer = Mojo::JSON::Pointer->new($json);
-	  foreach my $test (@$tests) {
-	    my ($type, $path, @rest) = @$test;
-
-	    # Check for equality
-	    if ($type eq 'is') {
-	      my $found = $json_pointer->get($path);
-	      if ($found && $found eq $rest[0]) {
-		$pass++;
-	      }
-	      else {
-		my $result = $path . q! isn't ! . shift @rest;
-		$result .= ' but was ' . $found if $found;
-		$result .= '; ' . join('; ', @rest);
-		push(@report, $result);
-		$fail++;
-	      };
-	    }
-
-	    # Check for inequality
-	    elsif ($type eq 'isnt') {
-	      if ($json_pointer->get($path) ne $rest[0]) {
-		$pass++;
-	      }
-	      else {
-		push(@report, $path . q! is ! . join('; ', @rest));
-		$fail++;
-	      };
-	    }
-
-	    # Check for existence
-	    elsif ($type eq 'ok') {
-	      if ($json_pointer->contains($path)) {
-		$pass++;
-	      }
-	      else {
-		push(@report, $path . q! doesn't exist; ! . join('; ', @rest));
-		$fail++;
-	      };
-	    }
-
-	    # Check for inexistence
-	    elsif ($type eq 'not_ok') {
-	      unless ($json_pointer->contains($path)) {
-		$pass++;
-	      }
-	      else {
-		push(@report, $path . q! doesn't exist; ! . join('; ', @rest));
-		$fail++;
-	      };
-	    };
-	  };
-	}
-	else {
-	  # There may be notifications here!
-	  $fail++ foreach @$tests;
-	};
-
-	# Emit hook to possible subscribers
-	# This is used for self-testing
-	# $plugin->emit_hook(korap_tut_query => (
-	#   query_language => $ql,
-	#   query => $q,
-	#   %param
-	# ));
-      };
+#	my $tests = $param{tests} // [];
+#	my $index = $c->search(
+#	  query => $q,
+#	  ql => $ql,
+#	  cutoff => 'true',
+#	  no_cache => 1
+#	);
+#
+#	# Get the raw results
+#	my $json = decode_json($index->api_response);
+#
+#	# There is a response
+#	if ($json) {
+#	  my $json_pointer = Mojo::JSON::Pointer->new($json);
+#	  foreach my $test (@$tests) {
+#	    my ($type, $path, @rest) = @$test;
+#
+#	    # Check for equality
+#	    if ($type eq 'is') {
+#	      my $found = $json_pointer->get($path);
+#	      if ($found && $found eq $rest[0]) {
+#		$pass++;
+#	      }
+#	      else {
+#		my $result = $path . q! isn't ! . shift @rest;
+#		$result .= ' but was ' . $found if $found;
+#		$result .= '; ' . join('; ', @rest);
+#		push(@report, $result);
+#		$fail++;
+#	      };
+#	    }
+#
+#	    # Check for inequality
+#	    elsif ($type eq 'isnt') {
+#	      if ($json_pointer->get($path) ne $rest[0]) {
+#		$pass++;
+#	      }
+#	      else {
+#		push(@report, $path . q! is ! . join('; ', @rest));
+#		$fail++;
+#	      };
+#	    }
+#
+#	    # Check for existence
+#	    elsif ($type eq 'ok') {
+#	      if ($json_pointer->contains($path)) {
+#		$pass++;
+#	      }
+#	      else {
+#		push(@report, $path . q! doesn't exist; ! . join('; ', @rest));
+#		$fail++;
+#	      };
+#	    }
+#
+#	    # Check for inexistence
+#	    elsif ($type eq 'not_ok') {
+#	      unless ($json_pointer->contains($path)) {
+#		$pass++;
+#	      }
+#	      else {
+#		push(@report, $path . q! doesn't exist; ! . join('; ', @rest));
+#		$fail++;
+#	      };
+#	    };
+#	  };
+#	}
+#	else {
+#	  # There may be notifications here!
+#	  $fail++ foreach @$tests;
+#	};
+#
+#	# Emit hook to possible subscribers
+#	# This is used for self-testing
+#	# $plugin->emit_hook(korap_tut_query => (
+#	#   query_language => $ql,
+#	#   query => $q,
+#	#   %param
+#	# ));
+ #     };
 
       # Escape query for html embedding
       $q = xml_escape $q;
diff --git a/t/api-v0.1.t b/t/api-v0.1.t
new file mode 100644
index 0000000..69e4d3f
--- /dev/null
+++ b/t/api-v0.1.t
@@ -0,0 +1,103 @@
+#!/usr/bin/env perl
+use strict;
+use warnings;
+use Test::More;
+use Test::Mojo;
+use Mojo::JSON;
+use Mojo::ByteStream 'b';
+use utf8;
+
+my $t = Test::Mojo->new;
+
+my $url = Mojo::URL->new('http://10.0.10.13:8888/api/v0.1/');
+
+# Get resources - corpus
+$t->get_ok($url->clone->path('corpus'))
+  ->status_is(200)
+  ->header_is('Content-Type', 'application/json; charset=utf-8')
+  ->json_is('/0/id', 'WPD')
+  ->json_is('/0/name', 'WPD')
+  ->json_is('/0/path', 'WPD')
+  ->json_is('/0/managed', Mojo::JSON->true)
+  ->json_is('/0/statistics/documents', 196510)
+  ->json_is('/0/statistics/tokens', 51545081)
+  ->json_is('/0/statistics/sentences', 4116282)
+  ->json_is('/0/statistics/paragraphs', 2034752);
+
+# Get resources - collection
+$t->get_ok($url->clone->path('collection'))
+  ->status_is(200)
+  ->json_is('/0/managed', Mojo::JSON->true)
+  ->json_is('/0/name', 'Wikipedia')
+  ->json_is('/0/description', 'Die freie Enzyklopädie');
+
+# Get resources - query
+$t->get_ok($url->clone->path('query'))
+  ->status_is(200)
+  ->json_is([]);
+
+# Get resources - foundry
+$t->get_ok($url->clone->path('foundry'))
+  ->status_is(200);
+
+# Get resources - layer
+$t->get_ok($url->clone->path('layer'))
+  ->status_is(200);
+
+# Stats
+$t->get_ok($url->clone->path('corpus/WPD/stats'))
+  ->json_is('/documents', 196510)
+  ->json_is('/tokens', 51545081)
+  ->json_is('/sentences', 4116282)
+  ->json_is('/paragraphs', 2034752)
+  ->status_is(200);
+
+# Matchinfo
+$t->get_ok($url->clone->path('corpus/WPD/SSS.04897/p29-30/matchInfo'))
+  ->json_is('/author', 'Darkone,Dramburg,Fusslkopp')
+  ->json_is('/textClass', 'freizeit-unterhaltung reisen')
+  ->json_is('/corpusID', 'WPD')
+  ->json_is('/title', 'Schloss Hohenzieritz')
+  ->json_is('/docID', 'WPD_SSS.04897')
+  ->json_is('/ID', 'match-WPD!WPD_SSS.04897-p29-30')
+  ->json_is('/snippet', "<span class=\"context-left\"><span class=\"more\"></span></span><span class=\"match\">Haus</span><span class=\"context-right\"><span class=\"more\"></span></span>")
+  ->status_is(200);
+
+# Matchinfo
+$t->get_ok($url->clone->path('corpus/WPD/SSS.04897/p29-30/matchInfo')->query({ foundry => '*'}))
+  ->json_is('/author', 'Darkone,Dramburg,Fusslkopp')
+  ->json_is('/textClass', 'freizeit-unterhaltung reisen')
+  ->json_is('/corpusID', 'WPD')
+  ->json_is('/title', 'Schloss Hohenzieritz')
+  ->json_is('/docID', 'WPD_SSS.04897')
+  ->json_is('/ID', 'match-WPD!WPD_SSS.04897-p23-45')
+  ->json_has('/snippet')
+  ->status_is(200);
+
+# Search
+# Check serialization
+$t->get_ok($url->clone->path('search')->query({ q => 'contains(<s>, [orth=Test])', ql => 'poliqarp'}))
+  ->json_is('/startIndex', 0)
+  ->json_like('/totalResults', qr/\d+/)
+  ->json_is('/itemsPerPage', 25)
+  ->status_is(200);
+
+my $tx = $t->ua->build_tx('TRACE', $url->clone->path('search')->query({ q => 'contains(<s>, [orth=Test])', ql => 'poliqarp'}));
+$tx = $t->ua->start($tx);
+
+#{"@context":"http://ids-mannheim.de/ns/KorAP/json-ld/v0.1/context.jsonld","query":{"@type":"korap:group","operation":"operation:position","frame":"frame:contains","operands":[{"@type":"korap:span","key":"s"},{"@type":"korap:token","wrap":{"@type":"korap:term","layer":"orth","key":"Test","match":"match:eq"}}]},"collections":[{"@type":"korap:meta-filter","@value":{"@type":"korap:term","@field":"korap:field#corpusID","@value":"WPD"}}],"meta":{}}
+$t->tx($tx)
+  ->json_is('/@context', 'http://ids-mannheim.de/ns/KorAP/json-ld/v0.2/context.jsonld')
+  ->json_is('/query/@type', 'korap:group')
+  ->json_is('/query/operation', 'operation:position')
+  ->json_is('/query/operands/0/@type', 'korap:span')
+  ->json_is('/query/operands/0/key', 's')
+  ->status_is(200);
+
+#$t->get_ok()
+#  ->content_is('')
+#  ->status_is(200);
+
+
+
+done_testing;
diff --git a/t/basic.t b/t/basic.t
index b24e2c0..c126a70 100644
--- a/t/basic.t
+++ b/t/basic.t
@@ -1,11 +1,13 @@
 use Mojo::Base -strict;
-
+use lib '../lib', 'lib';
 use Test::More;
 use Test::Mojo;
 
 my $t = Test::Mojo->new('Korap');
 $t->get_ok('/')
   ->status_is(200)
-  ->content_like(qr/Go to/i);
+  ->text_is('title', 'KorAP')
+  ->text_like('h1 span', qr/Korpusanalyseplattform/i)
+  ;
 
 done_testing();
diff --git a/t/non-blocking-api.t b/t/non-blocking-api.t
new file mode 100644
index 0000000..2d42281
--- /dev/null
+++ b/t/non-blocking-api.t
@@ -0,0 +1,220 @@
+
+use Mojo::Base -strict;
+use lib '../lib', 'lib';
+use Test::More;
+use Test::Mojo;
+use Mojo::URL;
+use Benchmark qw/:hireswallclock/;
+
+my $t = Test::Mojo->new('Korap');
+
+$t->app->routes->get('/searchtest')->to(
+  cb => sub {
+    my $c = shift;
+    $c->render(inline => <<'TEMPLATE');
+%= search query => param('q'), start_page => param('p'), no_cache => 1, begin
+<h1><%= search->query %></h1>
+<p id="api"><%= search->api %></p>
+<p id="cutoff"><%= search->cutoff %></p>
+<p id="ql"><%= search->query_language %></p>
+<p id="no_cache"><%= search->no_cache %></p>
+<p id="start_page"><%= search->start_page %></p>
+<p id="total_results"><%= search->total_results %></p>
+<p id="api_request"><%= search->api_request %></p>
+%=  search_results begin
+  <li><%= $_->{ID} %></li>
+%   end
+% end
+TEMPLATE
+  }
+);
+
+my $exttemplate = <<'EXTTEMPLATE';
+<h1><%= search->query %></h1>
+<p id="api"><%= search->api %></p>
+<p id="cutoff"><%= search->cutoff %></p>
+<p id="ql"><%= search->query_language %></p>
+<p id="no_cache"><%= search->no_cache %></p>
+<p id="start_page"><%= search->start_page %></p>
+<p id="total_results"><%= search->total_results %></p>
+<p id="api_request"><%= search->api_request %></p>
+%=  search_results begin
+  <li><%= $_->{ID} %></li>
+%   end
+EXTTEMPLATE
+
+
+$t->app->routes->get('/searchasync')->to(
+  cb => sub {
+    my $c = shift;
+    $c->search(
+      query => $c->param('q'),
+      start_page => $c->param('p'),
+      no_cache => 1,
+      cb => sub {
+	return $c->render(inline => $exttemplate);
+      }
+    );
+  }
+);
+
+my $tracetemplate = <<'TRACETEMPLATE';
+<h1><%= search->query %></h1>
+<p id="api"><%= search->api %></p>
+<p id="cutoff"><%= search->cutoff %></p>
+<p id="ql"><%= search->query_language %></p>
+<p id="api_request"><%= search->api_request %></p>
+<p id="query-jsonld"><%= dumper search->query_jsonld %></p>
+TRACETEMPLATE
+
+$t->app->routes->get('/traceasync')->to(
+  cb => sub {
+    my $c = shift;
+    $c->search->trace(
+      query => $c->param('q'),
+      sub {
+	return $c->render(inline => $tracetemplate);
+      }
+    );
+  }
+);
+
+my $matchtemplate = <<'MATCHTEMPLATE';
+<p id="api"><%= search->api %></p>
+<p id="api_request"><%= search->api_request %></p>
+<p id="search-result"><%= search->results->first->{docID} %></p>
+MATCHTEMPLATE
+
+
+$t->app->routes->get('/matchinfo')->to(
+  cb => sub {
+    my $c = shift;
+    $c->search->match(
+      corpus_id => $c->param('corpus_id'),
+      doc_id    => $c->param('doc_id'),
+      match_id  => $c->param('match_id'),
+      foundry => '*',
+      sub {
+	return $c->render(inline => $matchtemplate);
+      }
+    );
+  }
+);
+
+my $colltemplate = <<'COLLTEMPLATE';
+<p id="api"><%= search->api %></p>
+<p id="api_request"><%= search->api_request %></p>
+<p id="search-resource"><%= stash('search.resource')->[0]->{name} %></p>
+COLLTEMPLATE
+
+$t->app->routes->get('/collectioninfo')->to(
+  cb => sub {
+    my $c = shift;
+    $c->search->resource(
+      type => 'collection',
+      sub {
+	return $c->render(inline => $colltemplate);
+      }
+    );
+  }
+);
+
+
+$t->app->routes->get('/collectionandsearch-parallel')->to(
+  cb => sub {
+    my $c = shift;
+    $c->delay(
+      sub {
+	my $delay = shift;
+	$c->search->resource(
+	  type => 'collection',
+	  $delay->begin
+	);
+
+	$c->search(
+	  query => $c->param('q'),
+	  start_page => $c->param('p'),
+	  no_cache => 1,
+	  cb => $delay->begin
+	);
+      },
+      sub {
+	return $c->render(
+	  inline => $exttemplate .
+	    q!<p id="search-resource"><%= stash('search.resource')->[0]->{name} %></p>!
+	  );
+      }
+    )
+  }
+);
+
+my $query = 'startswith(<s>,[mate/m=gender:masc]{3,5})';
+
+# Search everything in parallel!
+$t->get_ok(Mojo::URL->new('/collectionandsearch-parallel')->query({q => $query}))
+  ->status_is(200)
+  ->text_is('.notify-error', '')
+  ->text_is('h1', $query)
+  ->text_is('#api', 'http://10.0.10.13:7070/api/v0.1/')
+  ->text_is('#ql', 'poliqarp')
+  ->text_is('#search-resource', 'Wikipedia');
+
+$t->get_ok('/collectioninfo')
+  ->status_is(200)
+  ->text_is('#search-resource', 'Wikipedia');
+
+
+# http://10.0.10.14:6666/corpus/WPD/WWW.04738/p265-266
+$t->get_ok(Mojo::URL->new('/matchinfo')->query({
+  corpus_id => 'WPD',
+  doc_id => 'WWW.04738',
+  match_id => 'p265-266'
+}))
+  ->status_is(200)
+  ->text_is('#search-result', 'WWW.04738');
+
+$t->get_ok(Mojo::URL->new('/traceasync')->query({q => $query}))
+  ->status_is(200)
+  ->text_is('.notify-error', '')
+  ->text_is('h1', $query)
+  ->text_is('#api', 'http://10.0.10.13:7070/api/v0.1/')
+  ->text_is('#ql', 'poliqarp')
+  ->text_like('#query-jsonld', qr!korap:boundary!);
+
+my $t0 = Benchmark->new;
+
+$t->get_ok(Mojo::URL->new('/searchasync')->query({q => $query}))
+  ->status_is(200)
+  ->text_is('.notify-error', '')
+  ->text_is('h1', $query)
+  ->text_is('#api', 'http://10.0.10.13:7070/api/v0.1/')
+  ->text_is('#cutoff', '')
+  ->text_is('#ql', 'poliqarp')
+  ->text_is('#no_cache', 1)
+  ->text_is('#start_page', 1)
+  ->text_is('#total_results', 54215)
+#  ->text_is('li', 'p265-266')
+  ;
+
+my $t1 = Benchmark->new;
+
+$t->get_ok(Mojo::URL->new('/searchtest')->query({q => $query}))
+  ->status_is(200)
+  ->text_is('.notify-error', '')
+  ->text_is('h1', $query)
+  ->text_is('#api', 'http://10.0.10.13:7070/api/v0.1/')
+  ->text_is('#cutoff', '')
+  ->text_is('#ql', 'poliqarp')
+  ->text_is('#no_cache', 1)
+  ->text_is('#start_page', 1)
+  ->text_is('#total_results', 54215)
+#  ->text_is('li', 'p265-266')
+  ;
+
+diag 'sync  ' . timestr(timediff(Benchmark->new, $t1));
+diag 'async ' . timestr(timediff($t1, $t0));
+
+# Check time_exceeded!
+
+done_testing;
+__END__
diff --git a/t/remote.t b/t/remote.t
new file mode 100644
index 0000000..9de6fb9
--- /dev/null
+++ b/t/remote.t
@@ -0,0 +1,31 @@
+use Mojo::Base -strict;
+use lib '../lib', 'lib';
+use Test::More;
+use Test::Mojo;
+
+my $t = Test::Mojo->new('Korap');
+$t->get_ok('/')
+  ->status_is(200)
+  ->text_is('title', 'KorAP')
+  ->text_like('h1 span', qr/Korpusanalyseplattform/i)
+  ;
+
+# Check paging
+$t->get_ok('/?q=test')
+  ->text_is('pre.query.serial span', 'JSON-LD Serialization for "test"')
+  ->text_like('#total-results', qr/\d+ matches$/)
+  ->text_is('#pagination a[rel=self] span', 1)
+;
+
+# Check paging
+$t->get_ok('/?q=test&p=2')
+  ->text_is('pre.query.serial span', 'JSON-LD Serialization for "test"')
+  ->text_like('#total-results', qr/\d+ matches$/)
+  ->text_is('#pagination a[rel=self] span', 2)
+;
+
+# $t->get_ok('/tutorial?testing=1')
+#   ->text_like('div.test p.pass', qr/Pass: [1-9]/)
+#  ;
+
+done_testing();
diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep
index 5f2951c..1d80316 100644
--- a/templates/layouts/default.html.ep
+++ b/templates/layouts/default.html.ep
@@ -97,10 +97,10 @@
 
 <main>
 %= content main => begin
-  <p>This is the alternative KorAP Frontend.</p>
+  <p>This is an alternative KorAP Frontend.</p>
   <p>The primary goal is to serve as a testbed for the query serialization and for different flavours of user interfaces.</p>
   <p>Search capabilities are limited to the demo user.</p>
-  <p>Currently the frontend only supports recent versions of Mozilla Firefox.</p>
+  <p>Currently the frontend is only tested for recent versions of Mozilla Firefox.</p>
 % end
 </main>
 
diff --git a/templates/query.html.ep b/templates/query.html.ep
index b0cd0a4..2ffee21 100644
--- a/templates/query.html.ep
+++ b/templates/query.html.ep
@@ -8,7 +8,7 @@
 %   };
 %   state $json = JSON::XS->new->allow_blessed->pretty->canonical(1);
 <pre class="query serial<%== $action // '' %>" <% unless ($action) { %>onclick="this.classList.toggle('active')"<% } %>>
-  <span>JSON-LD Serialization for <%= param 'q' %> (<%= param 'ql' %>)</span>
+  <span>JSON-LD Serialization for &quot;<%= param 'q' %>&quot;<% if (param('ql')) { %> (<%= param 'ql' %>)<% } %></span>
   <code>
 %# Workaround to keep true, false, and null intact
 %= $json->encode($json->decode(Mojo::JSON::encode_json(search->query_jsonld)))