Browse Source

Merge branch 'feature/bragi' into develop

* feature/bragi:
  Add search endpoint to render a search result.
  Add a model to hold a search result from Bragi
  Allow Base Model to be initialized from geojson hash
  Add a search query projector to handle requests through bragi
  Linter: Argument alignment in unrelated test
  Limit line length of conditional index creator.
  Ensure integration tests fail like minitest assertions intead of throwing errors
  Only create the custom gist index when it does not yet exist.
  Add autocomplete with Horsey using a custom backend service
develop
Bèr Kessels 3 months ago
parent
commit
e114839d8b

+ 3
- 2
Gemfile View File

@@ -9,6 +9,7 @@ gem 'bootstrap', '~> 4.3'
gem 'erubis'
gem 'event_sourcery'
gem 'event_sourcery-postgres', path: '../libs/event_sourcery-postgres'
gem 'faraday'
gem 'hashie'
gem 'json_expressions'
gem 'jsonapi-rb'
@@ -41,10 +42,10 @@ group :development, :test do
gem 'nokogiri'
gem 'puma' # Used by Capybara to run the web thread
gem 'rack-test'
gem 'rubocop'
gem 'selenium-webdriver'
gem 'timecop'

gem 'rubocop'
gem 'vcr'

gem 'awesome_print'
gem 'better_errors'

+ 6
- 0
Gemfile.lock View File

@@ -75,6 +75,8 @@ GEM
erubis (2.7.0)
event_sourcery (0.22.0)
execjs (2.7.0)
faraday (1.0.0)
multipart-post (>= 1.2, < 3)
ffi (1.11.1)
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
@@ -110,6 +112,7 @@ GEM
minitest
nokogiri
multi_json (1.13.1)
multipart-post (2.1.1)
mustermann (1.0.3)
nio4r (2.5.2)
nokogiri (1.8.5)
@@ -213,6 +216,7 @@ GEM
execjs (>= 0.3.0, < 3)
unicode-display_width (1.4.1)
uuidtools (2.1.5)
vcr (5.1.0)
xpath (3.2.0)
nokogiri (~> 1.8)
yajl (0.3.4)
@@ -233,6 +237,7 @@ DEPENDENCIES
erubis
event_sourcery
event_sourcery-postgres!
faraday
foreman
hashie
json_expressions
@@ -260,6 +265,7 @@ DEPENDENCIES
timecop
uglifier
uuidtools
vcr
yajl
yajl-ruby


+ 26
- 0
app/assets/javascripts/application.js View File

@@ -1 +1,27 @@
//= require "leaflet.js"
//= require "horsey.js"

var bragi = function(q, callback, base_url) {
r = new XMLHttpRequest();
r.open("GET", `${base_url}/autocomplete?q=${q}`, true);
r.onload = function() {
if (r.readyState != 4 || r.status != 200) return;
const data = JSON.parse(r.responseText);
const items = data.features.map(sourceEntry);

callback(null, [{ list: items }]);
}
r.send(q);
};

var sourceEntry = function(feature) {
var attrs = (feature.properties.geocoding || {});

return {
id: attrs.id,
poi_types: (attrs.poi_types || []).map(function(tp) { return tp.name }),
type: attrs.type,
city: ((attrs.address || {}).city || ''),
label: attrs.label
};
};

+ 3211
- 0
app/assets/javascripts/horsey.js
File diff suppressed because it is too large
View File


+ 2
- 0
app/assets/javascripts/horsey.min.js
File diff suppressed because it is too large
View File


+ 9
- 0
app/assets/stylesheets/application.scss View File

@@ -2,6 +2,7 @@

@import "bootstrap";
@import "leaflet.css";
@import "horsey.css";

#map {
height: 400px;
@@ -9,3 +10,11 @@
#map.full {
height: 800px;
}

.inline-icon {
height: 1em;
width: 1em;
}
.sey-selected .inline-icon {
filter: invert(100%);
}

+ 52
- 0
app/assets/stylesheets/horsey.css View File

@@ -0,0 +1,52 @@
.sey-container {
display: none;
position: absolute;
box-shadow: 1px 2px 6px;
background-color: #fff;
color: #333;
transition: left 0.1s ease-in-out;
z-index: 1;
}
.sey-list {
padding: 0;
margin: 0;
list-style-type: none;
}
.sey-show {
display: block;
}
.sey-hide {
display: none;
}
.sey-empty {
cursor: default;
padding: 7px;
}
.sey-item {
cursor: pointer;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding: 7px;
}
.sey-item:hover {
background-color: #444;
color: #fff;
}
.sey-selected {
background-color: #333;
color: #fff;
}
.sey-char-highlight {
font-weight: bold;
}
.sey-category-id {
background-color: #eee;
color: #aaa;
text-align: right;
text-transform: capitalize;
font-style: italic;
font-size: 12px;
box-shadow: 1px 0px 1px;
padding: 7px;
}

+ 1
- 0
app/assets/stylesheets/horsey.min.css View File

@@ -0,0 +1 @@
.sey-container{display:none;position:absolute;box-shadow:1px 2px 6px;background-color:#fff;color:#333;transition:left .1s ease-in-out;z-index:1}.sey-list{padding:0;margin:0;list-style-type:none}.sey-show{display:block}.sey-hide{display:none}.sey-empty{cursor:default;padding:7px}.sey-item{cursor:pointer;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;padding:7px}.sey-item:hover{background-color:#444;color:#fff}.sey-selected{background-color:#333;color:#fff}.sey-char-highlight{font-weight:700}.sey-category-id{background-color:#eee;color:#aaa;text-align:right;text-transform:capitalize;font-style:italic;font-size:12px;box-shadow:1px 0 1px;padding:7px}

+ 10
- 2
app/models/base.rb View File

@@ -5,11 +5,19 @@ module Hours
##
# Base model that can read from a query database
class Base
def initialize(attributes = {})
def initialize(attributes = {}, ignore_undefined_attributes = false)
attributes.each do |key, value|
send("#{key}=", value)
setter = "#{key}="
send(setter, value)
rescue NoMethodError
raise unless ignore_undefined_attributes
end
end

def self.from_geojson_feature(feature)
attrs = feature.properties.fetch('geocoding', {})
new(attrs, true)
end
end
end
end

+ 6
- 3
app/models/base_collection.rb View File

@@ -24,10 +24,13 @@ module Hours

def each
@dataset.each do |row|
if row
yield self.class::ITEM_CLASS.new(row)
else
case row
when NilClass
yield self.class::NULLITEM_CLASS.new
when Hash
yield self.class::ITEM_CLASS.new(row)
when RGeo::GeoJSON::Feature
yield self.class::ITEM_CLASS.from_geojson_feature(row)
end
end
end

+ 22
- 0
app/models/search_result.rb View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true

require_relative 'base.rb'
require_relative 'place_index.rb'

module Hours
module Models
##
# Region is a collection of Places belonging to one region.
# It is, therefore, a decorator for a list of places.
class SearchResult < PlaceIndex
attr_reader :query, :error

## TODO: PlaceIndex and other models need initialize with keyword args
def initialize(dataset: [], paginator: nil, query: '', error: nil)
@query = query
@error = error
super(dataset, paginator)
end
end
end
end

+ 3
- 1
app/projections/addresses.rb View File

@@ -37,7 +37,9 @@ module Hours
def index_gist
col = :location
name = "#{ADDRESS_TABLE_NAME}_#{col}"
"CREATE INDEX #{name} ON #{ADDRESS_TABLE_NAME} USING GIST(#{col})"
"CREATE INDEX IF NOT EXISTS #{name}
ON #{ADDRESS_TABLE_NAME}
USING GIST(#{col})"
end

# Directly imported with Postgres COPY CMD for now. So no projection

+ 65
- 0
app/projections/search_query.rb View File

@@ -0,0 +1,65 @@
# frozen_string_literal: true

require 'faraday'
require 'pagy'
require 'pagy/extras/array'
require 'rgeo/geo_json'

module Hours
module Projections
# Query handler that queries bragi, the rest backend for a string
class SearchQuery
include Pagy::Backend

attr_accessor :page

def self.build
faraday = Faraday.new(url: ENV['BRAGI_URL']) do |conn|
conn.response :raise_error
conn.adapter Faraday.default_adapter
end
new(faraday)
end

def initialize(http_client)
@http_client = http_client
end

def handle(query, page = 1)
@page = page
features = get_bragi_features(query)
pagy, places = pagy_array(features)
Hours::Models::SearchResult.new(
dataset: places,
paginator: pagy,
query: query
)
rescue Faraday::ServerError => e
Hours::Models::SearchResult.new(error: e.message)
end

protected

def pagy_array_get_vars(array, vars = {})
vars[:count] = array.size
vars[:page] = page
vars
end

private

def get_bragi_features(query)
response = @http_client.get('autocomplete') do |req|
req.params[:q] = query
end
RGeo::GeoJSON.decode(response.body).to_a
rescue JSON::ParserError
[]
end

def bragi_url(query)
"autocomplete?q=#{query}"
end
end
end
end

+ 52
- 0
app/views/home.erb View File

@@ -0,0 +1,52 @@
<%
js = "horsey(document.getElementById('search-input'), {
source (data, done) { bragi(data.input, done, '#{bragi_url}'); },
getText: 'label',
getValue: 'id',
renderItem: function(li, suggestion) {
var image = `<img class=\"inline-icon\" src=\"/images/${suggestion.type}.svg\" alt=\"${suggestion.type} icon\">`;
var type = `<small>${suggestion.poi_types.join(',')}</small>`;
var label = `${suggestion.label}`;
var city = `<small>${suggestion.city}</small>`;
li.innerHTML = `${image} ${label} ${city} ${type}`;
}
});"
set_content_for(:closing_js, js)
%>

<section class="container">
<div class="col justify-content-center">
<form action="/search" method="get">
<input autocomplete="off"
id="search-input"
name="q"
tabindex="1"
class="form-control form-control-lg"
type="search"
placeholder="bijv. Fietsenstalling, Nijmegen"
aria-label="Search for Places"/>
</form>
</div>
</section>
<hr>
<section class="row">
<div class="col">
<h2 class="title">Populaire plaatsen</h2>
<ul class="popular">
<li><a href="">Amsterdam</a></li>
<li><a href="">Rotterdam</a></li>
<li><a href="">Utrecht</a></li>
<li><a href="">Groningen</a></li>
<li><a href="/in/nijmegen">Nijmegen</a></li>
</ul>
</div>
<div class="col">
<h2 class="title">Recent gezocht</h2>
<ul class="popular">
<li><a href="">HEMA</a></li>
<li><a href="">Albert Heijn</a></li>
<li><a href="/in/nijmegen">Nijmegen</a></li>
<li><a href="">Tankstation Groningen</a></li>
</ul>
</div>
</section>

+ 21
- 0
app/views/search.erb View File

@@ -0,0 +1,21 @@
<h2>Zoekresultaat voor <%= @search_result.query %></h2>

<section class="row">
<div class="col-3 list-group">
<%- @search_result.each do |place| %>
<a href="/places/<%= place.id %>" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h3 class="mb-1 small"><%= place.name %></h3>
</div>
<p class="mb-1 small">
<%== erb :status_badge_partial, locals: { status: place.status } %>
<%= place.opening_hours %>
</p>
</a>
<% end %>
<%== pagy_bootstrap_nav(@search_result.paginator) %>
</div>
<div class="col">
<div id="map" class="full"></div>
</div>
</section>

+ 16
- 0
lib/app.rb View File

@@ -63,6 +63,13 @@ module Hours
# Autoescape HTML. Requires erubis
set :erb, escape_html: true

# Base URL where to find autocomplete server
helpers do
def bragi_url
ENV['BRAGI_URL']
end
end

error Hours::DuplicateError do |error|
unprocessable_entity(error)
end
@@ -112,6 +119,15 @@ module Hours
headers 'Location' => places_url(command.aggregate_id)
end

get '/', provides: 'html' do
erb :home
end

get '/search', provides: 'html' do
@search_result = Hours::Projections::SearchQuery.build.handle(params[:q])
erb :search
end

get '/in/:slug', provides: 'html' do
@region = Hours::Projections::RegionQuery.build.handle(
params[:slug],

+ 3
- 0
public/images/house.svg View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8">
<path d="M4 0l-4 3h1v4h2v-2h2v2h2v-4.03l1 .03-4-3z" />
</svg>

+ 1
- 0
public/images/poi.svg View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 14 20"><path d="M7 0C3.13 0 0 3.13 0 7c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5C5.62 9.5 4.5 8.38 4.5 7S5.62 4.5 7 4.5 9.5 5.62 9.5 7 8.38 9.5 7 9.5z"/></svg>

+ 3
- 0
public/images/street.svg View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" viewBox="0 0 8 8">
<path d="M3 0v1h-2l-1 1 1 1h2v5h1v-4h2l1-1-1-1h-2v-2h-1z" />
</svg>

+ 1
- 0
public/images/zone.svg View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 18 19"><path d="M12 9V3L9 0 6 3v2H0v14h18V9h-6zm-8 8H2v-2h2v2zm0-4H2v-2h2v2zm0-4H2V7h2v2zm6 8H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V7h2v2zm0-4H8V3h2v2zm6 12h-2v-2h2v2zm0-4h-2v-2h2v2z"/></svg>

+ 54
- 0
test/fixtures/vcr_cassettes/bragi_autocomplete_smullers.yml View File

@@ -0,0 +1,54 @@
---
http_interactions:
- request:
method: get
uri: http://localhost:4000/autocomplete?q=Smullers
body:
encoding: US-ASCII
string: ''
headers:
User-Agent:
- Faraday v1.0.0
response:
status:
code: 200
message: OK
headers:
content-length:
- '7932'
connection:
- close
content-type:
- application/json
cache-control:
- max-age=3600
date:
- Mon, 16 Mar 2020 18:11:03 GMT
body:
encoding: UTF-8
string: '{"type":"FeatureCollection","geocoding":{"version":"0.1.0","query":""},"features":[
{"type":"Feature","geometry":{"coordinates":[5.4796854999999995,51.4433638],"type":"Point"},"properties":{"geocoding":{"id":"poi:osm:node:692975604","type":"poi","label":"Smullers","name":"Smullers","postcode":null,"city":null,"citycode":null,"administrative_regions":[],"poi_types":[{"id":"poi_type:amenity:fast_food","name":"Fast-food"}],"properties":[{"key":"amenity","value":"fast_food"},{"key":"level","value":"0"},{"key":"name","value":"Smullers"}],"address":{"id":"addr:5.4798923;51.4432747:22-01","type":"house","label":"Stationsplein
22-01","name":"Stationsplein 22-01","housenumber":"22-01","street":"Stationsplein","postcode":"5611AC","city":null,"citycode":null,"administrative_regions":[]}}}},
{"type":"Feature","geometry":{"coordinates":[4.837769,52.389256],"type":"Point"},"properties":{"geocoding":{"id":"poi:osm:node:1282023633","type":"poi","label":"Smullers","name":"Smullers","postcode":null,"city":null,"citycode":null,"administrative_regions":[],"poi_types":[{"id":"poi_type:amenity:fast_food","name":"Fast-food"}],"properties":[{"key":"amenity","value":"fast_food"},{"key":"layer","value":"1"},{"key":"level","value":"1"},{"key":"name","value":"Smullers"},{"key":"takeaway","value":"yes"}],"address":{"id":"addr:4.8376742;52.3892327:103B","type":"house","label":"Orlyplein
103B","name":"Orlyplein 103B","housenumber":"103B","street":"Orlyplein","postcode":"1043DT","city":null,"citycode":null,"administrative_regions":[]}}}},
{"type":"Feature","geometry":{"coordinates":[4.899452999999999,52.379396],"type":"Point"},"properties":{"geocoding":{"id":"poi:osm:node:1539937895","type":"poi","label":"Smullers","name":"Smullers","postcode":null,"city":null,"citycode":null,"administrative_regions":[],"poi_types":[{"id":"poi_type:amenity:fast_food","name":"Fast-food"}],"properties":[{"key":"amenity","value":"fast_food"},{"key":"fast_food","value":"frites"},{"key":"level","value":"0"},{"key":"name","value":"Smullers"},{"key":"wheelchair","value":"yes"}],"address":{"id":"addr:4.8993813;52.3794034:45E","type":"house","label":"Stationsplein
45E","name":"Stationsplein 45E","housenumber":"45E","street":"Stationsplein","postcode":"1012AB","city":null,"citycode":null,"administrative_regions":[]}}}},
{"type":"Feature","geometry":{"coordinates":[4.482258,52.166436],"type":"Point"},"properties":{"geocoding":{"id":"poi:osm:node:1550278595","type":"poi","label":"Smullers","name":"Smullers","postcode":null,"city":null,"citycode":null,"administrative_regions":[],"poi_types":[{"id":"poi_type:amenity:fast_food","name":"Fast-food"}],"properties":[{"key":"amenity","value":"fast_food"},{"key":"cuisine","value":"fries"},{"key":"name","value":"Smullers"},{"key":"wheelchair","value":"yes"}],"address":{"id":"addr:4.4823144;52.1664861:3C","type":"house","label":"Stationsplein
3C","name":"Stationsplein 3C","housenumber":"3C","street":"Stationsplein","postcode":"2312AJ","city":null,"citycode":null,"administrative_regions":[]}}}},
{"type":"Feature","geometry":{"coordinates":[6.5669284999999995,53.2111502],"type":"Point"},"properties":{"geocoding":{"id":"poi:osm:node:1820935444","type":"poi","label":"Smullers","name":"Smullers","postcode":null,"city":null,"citycode":null,"administrative_regions":[],"poi_types":[{"id":"poi_type:amenity:fast_food","name":"Fast-food"}],"properties":[{"key":"amenity","value":"fast_food"},{"key":"name","value":"Smullers"}],"address":{"id":"addr:6.5669698;53.2111573:12","type":"house","label":"Stationsweg
12","name":"Stationsweg 12","housenumber":"12","street":"Stationsweg","postcode":"9726AC","city":null,"citycode":null,"administrative_regions":[]}}}},
{"type":"Feature","geometry":{"coordinates":[5.8531037999999995,51.8429894],"type":"Point"},"properties":{"geocoding":{"id":"poi:osm:node:2303136888","type":"poi","label":"Smullers","name":"Smullers","postcode":null,"city":null,"citycode":null,"administrative_regions":[],"poi_types":[{"id":"poi_type:amenity:fast_food","name":"Fast-food"}],"properties":[{"key":"amenity","value":"fast_food"},{"key":"name","value":"Smullers"}],"address":{"id":"addr:5.8530919;51.843104:6P","type":"house","label":"Stationsplein
6P","name":"Stationsplein 6P","housenumber":"6P","street":"Stationsplein","postcode":"6512AB","city":null,"citycode":null,"administrative_regions":[]}}}},
{"type":"Feature","geometry":{"coordinates":[4.6684972,51.8074112],"type":"Point"},"properties":{"geocoding":{"id":"poi:osm:node:2312666233","type":"poi","label":"Smullers","name":"Smullers","postcode":null,"city":null,"citycode":null,"administrative_regions":[],"poi_types":[{"id":"poi_type:amenity:fast_food","name":"Fast-food"}],"properties":[{"key":"amenity","value":"fast_food"},{"key":"name","value":"Smullers"}],"address":{"id":"addr:4.668338;51.8075061:1","type":"house","label":"Stationsplein
1","name":"Stationsplein 1","housenumber":"1","street":"Stationsplein","postcode":"3311JV","city":null,"citycode":null,"administrative_regions":[]}}}},
{"type":"Feature","geometry":{"coordinates":[5.1089381,52.089424],"type":"Point"},"properties":{"geocoding":{"id":"poi:osm:node:2449612553","type":"poi","label":"Smullers","name":"Smullers","postcode":"3511CB","city":null,"citycode":null,"administrative_regions":[],"poi_types":[{"id":"poi_type:amenity:fast_food","name":"Fast-food"}],"properties":[{"key":"addr:city","value":"Utrecht"},{"key":"addr:housenumber","value":"7b"},{"key":"addr:postcode","value":"3511CB"},{"key":"addr:street","value":"Stationspassage"},{"key":"amenity","value":"fast_food"},{"key":"layer","value":"2"},{"key":"level","value":"1"},{"key":"name","value":"Smullers"},{"key":"source","value":"BAG"},{"key":"source:date","value":"2016-03-22"},{"key":"toilets:wheelchair","value":"no"},{"key":"wheelchair","value":"yes"}],"address":{"id":"addr:5.1088784;52.0893902:10","type":"house","label":"Stationspassage
10","name":"Stationspassage 10","housenumber":"10","street":"Stationspassage","postcode":"3511CB","city":null,"citycode":null,"administrative_regions":[]}}}},
{"type":"Feature","geometry":{"coordinates":[6.0916635,52.5053706],"type":"Point"},"properties":{"geocoding":{"id":"poi:osm:node:2497603411","type":"poi","label":"Smullers","name":"Smullers","postcode":"8011CW","city":null,"citycode":null,"administrative_regions":[],"poi_types":[{"id":"poi_type:amenity:fast_food","name":"Fast-food"}],"properties":[{"key":"addr:city","value":"Zwolle"},{"key":"addr:housenumber","value":"22"},{"key":"addr:postcode","value":"8011CW"},{"key":"addr:street","value":"Stationsplein"},{"key":"amenity","value":"fast_food"},{"key":"name","value":"Smullers"},{"key":"source","value":"BAG"},{"key":"source:date","value":"2013-09-08"}],"address":{"id":"addr:6.0916966;52.505363:22","type":"house","label":"Stationsplein
22","name":"Stationsplein 22","housenumber":"22","street":"Stationsplein","postcode":"8011CW","city":null,"citycode":null,"administrative_regions":[]}}}},
{"type":"Feature","geometry":{"coordinates":[4.4689977,51.924987599999994],"type":"Point"},"properties":{"geocoding":{"id":"poi:osm:node:2610276000","type":"poi","label":"Smullers","name":"Smullers","postcode":"3013AJ","city":null,"citycode":null,"administrative_regions":[],"poi_types":[{"id":"poi_type:amenity:fast_food","name":"Fast-food"}],"properties":[{"key":"addr:city","value":"Rotterdam"},{"key":"addr:housenumber","value":"8A"},{"key":"addr:postcode","value":"3013AJ"},{"key":"addr:street","value":"Stationsplein"},{"key":"amenity","value":"fast_food"},{"key":"name","value":"Smullers"},{"key":"operator","value":"NS
Stations"},{"key":"source","value":"BAG"},{"key":"source:date","value":"2013-11-26"},{"key":"wheelchair","value":"yes"}],"address":{"id":"addr:4.4689977;51.9249876:8A","type":"house","label":"Stationsplein
8A","name":"Stationsplein 8A","housenumber":"8A","street":"Stationsplein","postcode":"3013AJ","city":null,"citycode":null,"administrative_regions":[]}}}}
]}'
http_version: null
recorded_at: Mon, 16 Mar 2020 18:11:03 GMT
recorded_with: VCR 5.1.0

+ 19
- 0
test/integration/web/searches_places_test.rb View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true

require 'test_helper'

describe 'web view places' do
include WebTestHelpers

describe 'GET /search?q=<term>' do
it 'returns search results from bragi' do
VCR.use_cassette(:bragi_autocomplete_smullers) do
visit '/search?q=Smullers'
end

assert_equal 200, page.driver.status_code
assert_selector page, 'h2', text: 'Zoekresultaat voor Smullers'
assert_selector page, 'a.list-group-item', text: 'Smullers'
end
end
end

+ 21
- 20
test/integration/web/view_places_test.rb View File

@@ -15,34 +15,35 @@ describe 'web view places' do
it 'returns a node detail page' do
Timecop.travel(die_wende) do
visit "/places/#{hm_id}"
page.assert_selector('html[lang=nl]')
assert_selector page, 'html[lang=nl]'

# SEO and title
assert page.has_title?('Openingstijden')
page.assert_selector('h1.title', text: 'Openingstijden')
assert_title page, 'Openingstijden'
assert_selector page, 'h1.title', text: 'Openingstijden'

# Breadcrumb
page.assert_selector('nav ol.breadcrumb a', text: 'Openingstijden')
page.assert_selector('nav ol.breadcrumb a', text: 'in Nijmegen')
page.assert_selector('nav ol.breadcrumb li.active', text: 'H&M')
assert_selector page, 'nav ol.breadcrumb a', text: 'Openingstijden'
assert_selector page, 'nav ol.breadcrumb a', text: 'in Nijmegen'
assert_selector page, 'nav ol.breadcrumb li.active', text: 'H&M'

# Name and Address
page.assert_selector('h2', text: 'H&M')
page.assert_selector('address',
text: 'Burchtstraat 1 , 6511RA Nijmegen')
assert_selector page, 'h2', text: 'H&M'
assert_selector page,
'address',
text: 'Burchtstraat 1 , 6511RA Nijmegen'

# A badge with class success means open.
page.assert_selector('span.badge-success', text: 'Open')
assert_selector page, 'span.badge-success', text: 'Open'

within('table#opening_hours') do
page.assert_selector('tr', text: 'Ma 10:00 18:00')
page.assert_selector('tr', text: 'Zo 12:00 17:30')
assert_selector page, 'tr', text: 'Ma 10:00 18:00'
assert_selector page, 'tr', text: 'Zo 12:00 17:30'

# Current day is Thursday. Current Day is bold
assert_equal 4, die_wende.wday
page.assert_selector('tr>th>strong', text: 'Do')
page.assert_selector('tr>td>strong', text: '10:00')
page.assert_selector('tr>td>strong', text: '21:00')
assert_selector page, 'tr>th>strong', text: 'Do'
assert_selector page, 'tr>td>strong', text: '10:00'
assert_selector page, 'tr>td>strong', text: '21:00'
end
end
end
@@ -52,7 +53,7 @@ describe 'web view places' do
Timecop.travel(after_hours) do
visit "/places/#{hm_id}"
# A badge with class danger means closed.
page.assert_selector('span.badge-danger', text: 'Gesloten')
assert_selector page, 'span.badge-danger', text: 'Gesloten'
end
end
end
@@ -65,7 +66,7 @@ describe 'web view places' do
Timecop.travel(die_wende) do
visit "/places/#{hm_id}"
# A badge with class secondary means unknown.
page.assert_selector('span.badge-secondary', text: 'Onbekend')
assert_selector page, 'span.badge-secondary', text: 'Onbekend'
end
end
end
@@ -75,9 +76,9 @@ describe 'web view places' do

it 'escapes ugly attributes' do
visit "/places/#{hm_id}"
page.assert_selector('nav ol.breadcrumb li.active', text: 'H@ckedM')
page.assert_selector('h2', text: 'H@ckedM')
page.assert_no_selector('address script', visible: false)
assert_selector page, 'nav ol.breadcrumb li.active', text: 'H@ckedM'
assert_selector page, 'h2', text: 'H@ckedM'
assert_no_selector page, 'address script', visible: false
end
end


+ 20
- 19
test/integration/web/view_regions_test.rb View File

@@ -26,10 +26,10 @@ describe 'web views regions' do
end

it 'follows link to index' do
page.assert_current_path 'http://www.example.com/in/nijmegen'
assert_current_path page, 'http://www.example.com/in/nijmegen'

page.assert_selector('h2', text: 'Openingstijden in Nijmegen')
page.assert_selector('h3', text: 'H&M')
assert_selector page, 'h2', text: 'Openingstijden in Nijmegen'
assert_selector page, 'h3', text: 'H&M'
end

describe 'with 21 items in nijmegen' do
@@ -38,21 +38,21 @@ describe 'web views regions' do
let(:input) { list_of_geojson(21, nijmegen.geometry).reverse }

it 'pages the index' do
page.assert_current_path 'http://www.example.com/in/nijmegen'
assert_current_path page, 'http://www.example.com/in/nijmegen'
assert_equal 20, page.find_all('a.list-group-item').length

within 'ul.pagination' do
click_link '2'
end

page.assert_current_path 'http://www.example.com/in/nijmegen?page=2'
assert_current_path page, 'http://www.example.com/in/nijmegen?page=2'
assert_equal 1, page.find_all('a.list-group-item').length
end

it 'sorts them from the center of "Nijmegen" do' do
items = page.find_all('a.list-group-item')
items[0].assert_text 'place #0'
items[1].assert_text 'place #1'
assert_text items[0], 'place #0'
assert_text items[1], 'place #1'
end
end
end
@@ -71,11 +71,12 @@ describe 'web views regions' do
it 'sorts them from the centroid of Arnhem' do
visit '/in/arnhem'

page.assert_no_text 'No places found in Arnhem'
assert_no_text page, 'No places found in Arnhem'

items = page.find_all('a.list-group-item')
# Ecoplaza is nearest to centroid
items[0].assert_text 'Ekoplaza'
items[1].assert_text 'H&M'
assert_text items[0], 'Ekoplaza'
assert_text items[1], 'H&M'
end
end

@@ -85,7 +86,7 @@ describe 'web views regions' do

it 'supports tiny hamlets' do
visit '/in/zuidwolde'
page.assert_text 'Moeke Vaatstra'
assert_text page, 'Moeke Vaatstra'
end
end

@@ -115,19 +116,19 @@ describe 'web views regions' do
it 'has a region zuidwolde' do
visit '/in/zuidwolde'

page.assert_text 'Moeke Vaatstra'
page.assert_text "Bed en brood 'Irene'"
assert_text page, 'Moeke Vaatstra'
assert_text page, "Bed en brood 'Irene'"

page.assert_no_text 'Grillroom Yusuf'
assert_no_text page, 'Grillroom Yusuf'
end

it 'has a region zuidwolde-1' do
visit '/in/zuidwolde-1'

page.assert_text 'Grillroom Yusuf'
assert_text page, 'Grillroom Yusuf'

page.assert_no_text 'Moeke Vaatstra'
page.assert_no_text "Bed en brood 'Irene'"
assert_no_text page, 'Moeke Vaatstra'
assert_no_text page, "Bed en brood 'Irene'"
end
end

@@ -138,7 +139,7 @@ describe 'web views regions' do
it 'Renders region name with UTF8 but slug is normalized' do
visit "/places/#{aggregate_id}"
click_link "'s-Hertogenbosch"
page.assert_current_path 'http://www.example.com/in/s-hertogenbosch'
assert_current_path page, 'http://www.example.com/in/s-hertogenbosch'
end
end

@@ -149,7 +150,7 @@ describe 'web views regions' do
it 'shows empty text' do
visit '/in/eindhoven'

page.assert_text('No places found in Eindhoven')
assert_text page, 'No places found in Eindhoven'
end
end
end

+ 30
- 0
test/models/base_test.rb View File

@@ -4,4 +4,34 @@ require 'test_helper'
require_relative Hours.base_path.join 'app/models/base.rb'

describe Hours::Models::Base do
class MockModel < Hours::Models::Base
attr_accessor :name
end

describe '.initialize' do
it 'sets accessible attributes' do
subject = MockModel.new(name: 'Hi')
assert_equal 'Hi', subject.name
end

it 'raises on undefined methods' do
assert_raises NoMethodError do
MockModel.new(fakemethod: 'val')
end
end
end

describe '.from_geojson_feature' do
let(:feature) do
OpenStruct.new(
properties: { 'geocoding' => { 'name' => 'Hi', 'fakemethod' => 'val' } }
)
end

it 'ignores undefined attributes' do
subject = MockModel.from_geojson_feature(feature)
# Does not raise NoMethodError
assert_equal 'Hi', subject.name
end
end
end

+ 36
- 0
test/models/search_result_test.rb View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true

require 'test_helper'

describe Hours::Models::Region do
let(:row) { { id: 'PK' } }
let(:dataset) { [row] }

subject do
Hours::Models::SearchResult.new(dataset: dataset)
end

it 'is a collection of places' do
assert_kind_of Hours::Models::Place, subject.first
end

describe '#query' do
subject do
Hours::Models::SearchResult.new(dataset: dataset, query: 'Smullers')
end

it 'returns the passed-in query' do
assert_equal 'Smullers', subject.query
end
end

describe '#error' do
subject do
Hours::Models::SearchResult.new(dataset: dataset, error: 'An Error')
end

it 'returns the passed-in error' do
assert_equal 'An Error', subject.error
end
end
end

+ 42
- 0
test/projections/search_query_test.rb View File

@@ -0,0 +1,42 @@
# frozen_string_literal: true

require 'test_helper'

describe Hours::Projections::SearchQuery do
let(:bragi_response) { OpenStruct.new(body: '{}', code: 200) }
let(:error_response) { OpenStruct.new(body: '', code: 500) }

let(:http_client) do
Minitest::Mock.new
end

subject do
Hours::Projections::SearchQuery.new(http_client)
end

it 'requests from HTTP service' do
http_client.expect(:get, bragi_response, ['autocomplete'])

assert_kind_of Hours::Models::SearchResult, subject.handle('Smullers')

assert_mock http_client
end

it 'handles broken body from HTTP service' do
http_client.expect(:get, error_response, [String])

assert_kind_of Hours::Models::SearchResult, subject.handle('Smullers')

assert_mock http_client
end

it 'handles HTTP errors from HTTP service' do
error = ->(_args) { raise Faraday::ServerError, '503' }
http_client = OpenStruct.new(get: '')

http_client.stub(:get, error) do
result = Hours::Projections::SearchQuery.new(http_client).handle('')
assert_equal '503', result.error
end
end
end

+ 7
- 0
test/support/web_test_helpers.rb View File

@@ -2,11 +2,13 @@

require 'capybara'
require 'capybara/dsl'
require 'capybara/minitest'
require 'json_expressions/minitest'
require 'minitest/have_tag'

module WebTestHelpers
include Capybara::DSL
include Capybara::Minitest::Assertions

def setup
super
@@ -14,4 +16,9 @@ module WebTestHelpers
Capybara.default_driver = :rack_test
Capybara.app = Hours::Web
end

def teardown
Capybara.reset_sessions!
Capybara.use_default_driver
end
end

+ 7
- 1
test/test_helper.rb View File

@@ -2,12 +2,13 @@

require 'minitest/autorun'
require 'database_cleaner'
require 'byebug'
require 'vcr'

require 'hours'
require 'app'

require 'awesome_print'
require 'byebug'
require 'ostruct'

ENV['APP_ENV'] = ENV['RACK_ENV'] = 'test'
@@ -38,6 +39,11 @@ module Minitest
DatabaseCleaner.strategy = :truncation,
{ except: %w[spatial_ref_sys query_addresses] }

VCR.configure do |config|
config.cassette_library_dir = 'test/fixtures/vcr_cassettes'
config.hook_into :faraday
end

before :each do
DatabaseCleaner.start
end

BIN
vendor/cache/faraday-1.0.0.gem View File


BIN
vendor/cache/multipart-post-2.1.1.gem View File


BIN
vendor/cache/vcr-5.1.0.gem View File


Loading…
Cancel
Save