algoliasearch-jekyll 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
data/spec/push_spec.rb ADDED
@@ -0,0 +1,220 @@
1
+ require 'spec_helper'
2
+
3
+ describe(AlgoliaSearchJekyllPush) do
4
+ let(:push) { AlgoliaSearchJekyllPush }
5
+ let(:site) { get_site }
6
+ let(:page_file) { site.file_by_name('about.md') }
7
+ let(:html_page_file) { site.file_by_name('authors.html') }
8
+ let(:excluded_page_file) { site.file_by_name('excluded.html') }
9
+ let(:post_file) { site.file_by_name('2015-07-02-test-post.md') }
10
+ let(:static_file) { site.file_by_name('ring.png') }
11
+ let(:document_file) { site.file_by_name('collection-item.md') }
12
+ let(:html_document_file) { site.file_by_name('collection-item.html') }
13
+ let(:options) do
14
+ {
15
+ 'drafts' => true
16
+ }
17
+ end
18
+ let(:config) do
19
+ {
20
+ 'source' => File.expand_path('./spec/fixtures'),
21
+ 'markdown_ext' => 'md,mkd',
22
+ 'algolia' => {
23
+ 'application_id' => 'APPID',
24
+ 'index_name' => 'INDEXNAME'
25
+ }
26
+ }
27
+ end
28
+
29
+ describe 'init_options' do
30
+ it 'sets options and config' do
31
+ # Given
32
+ args = nil
33
+
34
+ # When
35
+ push.init_options(args, options, config)
36
+
37
+ # Then
38
+ expect(push.options).to include(options)
39
+ expect(push.config).to include(config)
40
+ end
41
+
42
+ it 'sets indexname from the commandline' do
43
+ # Given
44
+ args = ['newindex']
45
+
46
+ # When
47
+ push.init_options(args, options, config)
48
+
49
+ # Then
50
+ expect(push.config['algolia']['index_name']).to eq 'newindex'
51
+ end
52
+ end
53
+
54
+ describe 'indexable?' do
55
+ before(:each) do
56
+ push.init_options(nil, options, config)
57
+ end
58
+
59
+ it 'exclude StaticFiles' do
60
+ expect(push.indexable?(static_file)).to eq false
61
+ end
62
+
63
+ it 'keeps markdown files' do
64
+ expect(push.indexable?(page_file)).to eq true
65
+ end
66
+
67
+ it 'keeps html files' do
68
+ expect(push.indexable?(html_page_file)).to eq true
69
+ end
70
+
71
+ it 'keeps markdown documents' do
72
+ expect(push.indexable?(document_file)).to eq true
73
+ end
74
+
75
+ it 'keeps html documents' do
76
+ expect(push.indexable?(html_document_file)).to eq true
77
+ end
78
+
79
+ it 'exclude file specified in config' do
80
+ # Given
81
+ config['algolia']['excluded_files'] = [
82
+ 'excluded.html'
83
+ ]
84
+ push.init_options(nil, options, config)
85
+
86
+ # Then
87
+ expect(push.indexable?(excluded_page_file)).to eq false
88
+ end
89
+ end
90
+
91
+ describe 'api_key' do
92
+ it 'returns nil if no key found' do
93
+ # Given
94
+ push.init_options(nil, options, config)
95
+
96
+ # When
97
+ expect(push.api_key).to be_nil
98
+ end
99
+
100
+ it 'reads from ENV var if set' do
101
+ # Given
102
+ push.init_options(nil, options, config)
103
+ stub_const('ENV', 'ALGOLIA_API_KEY' => 'APIKEY_FROM_ENV')
104
+
105
+ # When
106
+ actual = push.api_key
107
+
108
+ # Then
109
+ expect(actual).to eq 'APIKEY_FROM_ENV'
110
+ end
111
+
112
+ it 'reads from _algolia_api_key in source if set' do
113
+ # Given
114
+ config['source'] = File.join(config['source'], 'api_key_dir')
115
+ push.init_options(nil, options, config)
116
+
117
+ # When
118
+ actual = push.api_key
119
+
120
+ # Then
121
+ expect(actual).to eq 'APIKEY_FROM_FILE'
122
+ end
123
+
124
+ it 'reads from ENV before from file' do
125
+ # Given
126
+ config['source'] = File.join(config['source'], 'api_key_dir')
127
+ stub_const('ENV', 'ALGOLIA_API_KEY' => 'APIKEY_FROM_ENV')
128
+ push.init_options(nil, options, config)
129
+
130
+ # When
131
+ actual = push.api_key
132
+
133
+ # Then
134
+ expect(actual).to eq 'APIKEY_FROM_ENV'
135
+ end
136
+ end
137
+
138
+ describe 'check_credentials' do
139
+ it 'should display error if no api key' do
140
+ # Given
141
+ config['algolia'] = {
142
+ 'application_id' => 'APP_ID',
143
+ 'index_name' => 'INDEX_NAME'
144
+ }
145
+ push.init_options(nil, options, config)
146
+
147
+ # Then
148
+ expect(Jekyll.logger).to receive(:error).with(/api key/i)
149
+ expect(Jekyll.logger).to receive(:warn).at_least(:once)
150
+ expect(-> { push.check_credentials }).to raise_error SystemExit
151
+ end
152
+
153
+ it 'should display error if no application id' do
154
+ # Given
155
+ config['algolia'] = {
156
+ 'application_id' => nil,
157
+ 'index_name' => 'INDEX_NAME'
158
+ }
159
+ stub_const('ENV', 'ALGOLIA_API_KEY' => 'APIKEY_FROM_ENV')
160
+ push.init_options(nil, options, config)
161
+
162
+ # Then
163
+ expect(Jekyll.logger).to receive(:error).with(/application id/i)
164
+ expect(Jekyll.logger).to receive(:warn).at_least(:once)
165
+ expect(-> { push.check_credentials }).to raise_error SystemExit
166
+ end
167
+
168
+ it 'should display error if no index name' do
169
+ # Given
170
+ config['algolia'] = {
171
+ 'application_id' => 'APPLICATION_ID',
172
+ 'index_name' => nil
173
+ }
174
+ stub_const('ENV', 'ALGOLIA_API_KEY' => 'APIKEY_FROM_ENV')
175
+ push.init_options(nil, options, config)
176
+
177
+ # Then
178
+ expect(Jekyll.logger).to receive(:error).with(/index name/i)
179
+ expect(Jekyll.logger).to receive(:warn).at_least(:once)
180
+ expect(-> { push.check_credentials }).to raise_error SystemExit
181
+ end
182
+ end
183
+
184
+ describe 'configure_index' do
185
+ it 'sets some sane defaults' do
186
+ # Given
187
+ push.init_options(nil, options, config)
188
+ index = double
189
+
190
+ # Then
191
+ expected = {
192
+ attributeForDistinct: 'title',
193
+ distinct: true,
194
+ customRanking: ['desc(posted_at)', 'desc(weight)']
195
+ }
196
+ expect(index).to receive(:set_settings).with(hash_including(expected))
197
+
198
+ # When
199
+ push.configure_index(index)
200
+ end
201
+
202
+ it 'allow user to override all settings' do
203
+ # Given
204
+ settings = {
205
+ distinct: false,
206
+ customSetting: 'foo',
207
+ customRanking: ['asc(foo)', 'desc(bar)']
208
+ }
209
+ config['algolia']['settings'] = settings
210
+ push.init_options(nil, options, config)
211
+ index = double
212
+
213
+ # Then
214
+ expect(index).to receive(:set_settings).with(hash_including(settings))
215
+
216
+ # When
217
+ push.configure_index(index)
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,451 @@
1
+ require 'spec_helper'
2
+
3
+ describe(AlgoliaSearchRecordExtractor) do
4
+ let(:extractor) { AlgoliaSearchRecordExtractor }
5
+ let(:site) { get_site }
6
+ let(:page_file) { extractor.new(site.file_by_name('about.md')) }
7
+ let(:html_page_file) { extractor.new(site.file_by_name('authors.html')) }
8
+ let(:post_file) { extractor.new(site.file_by_name('test-post.md')) }
9
+ let(:hierarchy_page_file) { extractor.new(site.file_by_name('hierarchy.md')) }
10
+ let(:weight_page_file) { extractor.new(site.file_by_name('weight.md')) }
11
+ let(:document_file) { extractor.new(site.file_by_name('collection-item.md')) }
12
+
13
+ describe 'metadata' do
14
+ it 'gets metadata from page' do
15
+ # Given
16
+ actual = page_file.metadata
17
+
18
+ # Then
19
+ expect(actual[:type]).to eq 'page'
20
+ expect(actual[:slug]).to eq 'about'
21
+ expect(actual[:title]).to eq 'About page'
22
+ expect(actual[:url]).to eq '/about.html'
23
+ expect(actual[:custom]).to eq 'Foo'
24
+ end
25
+
26
+ it 'gets metadata from post' do
27
+ # Given
28
+ actual = post_file.metadata
29
+
30
+ # Then
31
+ expect(actual[:type]).to eq 'post'
32
+ expect(actual[:slug]).to eq 'test-post'
33
+ expect(actual[:title]).to eq 'Test post'
34
+ expect(actual[:url]).to eq '/2015/07/02/test-post.html'
35
+ expect(actual[:posted_at]).to eq 1_435_788_000
36
+ expect(actual[:custom]).to eq 'Foo'
37
+ end
38
+
39
+ it 'gets metadata from document' do
40
+ # Given
41
+ actual = document_file.metadata
42
+
43
+ # Then
44
+ expect(actual[:type]).to eq 'document'
45
+ expect(actual[:slug]).to eq 'collection-item'
46
+ expect(actual[:title]).to eq 'Collection Item'
47
+ expect(actual[:url]).to eq '/my-collection/collection-item.html'
48
+ expect(actual[:custom]).to eq 'Foo'
49
+ end
50
+ end
51
+
52
+ describe 'tags' do
53
+ it 'returns nil if no tag found' do
54
+ expect(page_file.tags).to eq nil
55
+ end
56
+ it 'extract tags from front matter' do
57
+ # Given
58
+ actual = post_file.tags
59
+
60
+ # Then
61
+ expect(actual).to include('tag', 'another tag')
62
+ end
63
+ end
64
+
65
+ describe 'html_nodes' do
66
+ it 'returns the list of all <p> by default' do
67
+ expect(page_file.html_nodes.size).to eq 6
68
+ end
69
+
70
+ it 'allow _config.yml to override the selector' do
71
+ # Given
72
+ site = get_site(algolia: { 'record_css_selector' => 'p,ul' })
73
+ page_file = extractor.new(site.file_by_name('about.md'))
74
+
75
+ expect(page_file.html_nodes.size).to eq 7
76
+ end
77
+ end
78
+
79
+ describe 'node_heading_parent' do
80
+ it 'returns the direct heading right above' do
81
+ # Given
82
+ nodes = hierarchy_page_file.html_nodes
83
+ p = nodes[0]
84
+
85
+ # When
86
+ actual = hierarchy_page_file.node_heading_parent(p)
87
+
88
+ # Then
89
+ expect(actual.name).to eq 'h1'
90
+ expect(actual.text).to eq 'H1'
91
+ end
92
+
93
+ it 'returns the closest heading even if in a sub tag' do
94
+ # Given
95
+ nodes = hierarchy_page_file.html_nodes
96
+ p = nodes[2]
97
+
98
+ # When
99
+ actual = hierarchy_page_file.node_heading_parent(p)
100
+
101
+ # Then
102
+ expect(actual.name).to eq 'h2'
103
+ expect(actual.text).to eq 'H2A'
104
+ end
105
+
106
+ it 'should automatically go up one level when indexing headings' do
107
+ # Given
108
+ site = get_site(algolia: { 'record_css_selector' => 'p,h2' })
109
+ hierarchy_page_file = extractor.new(site.file_by_name('hierarchy.md'))
110
+ nodes = hierarchy_page_file.html_nodes
111
+ h2 = nodes[4]
112
+
113
+ # When
114
+ actual = hierarchy_page_file.node_heading_parent(h2)
115
+
116
+ # Then
117
+ expect(actual.name).to eq 'h1'
118
+ expect(actual.text).to eq 'H1'
119
+ end
120
+
121
+ it 'should find the correct parent when indexing deep headings' do
122
+ # Given
123
+ site = get_site(algolia: { 'record_css_selector' => 'h2' })
124
+ hierarchy_page_file = extractor.new(site.file_by_name('hierarchy.md'))
125
+ nodes = hierarchy_page_file.html_nodes
126
+ h2 = nodes[2]
127
+
128
+ # When
129
+ actual = hierarchy_page_file.node_heading_parent(h2)
130
+
131
+ # Then
132
+ expect(actual.name).to eq 'h1'
133
+ expect(actual.text).to eq 'H1'
134
+ end
135
+ end
136
+
137
+ describe 'node_hierarchy' do
138
+ it 'returns the unique parent of a simple element' do
139
+ # Note: First <p> should only have a h1 as hierarchy
140
+ # Given
141
+ nodes = hierarchy_page_file.html_nodes
142
+ p = nodes[0]
143
+
144
+ # When
145
+ actual = hierarchy_page_file.node_hierarchy(p)
146
+
147
+ # Then
148
+ expect(actual).to include(h1: 'H1')
149
+ end
150
+
151
+ it 'returns the heading hierarchy of multiple headings' do
152
+ # Note: 5th <p> is inside h3, second h2 and main h1
153
+ # Given
154
+ nodes = hierarchy_page_file.html_nodes
155
+ p = nodes[4]
156
+
157
+ # When
158
+ actual = hierarchy_page_file.node_hierarchy(p)
159
+
160
+ # Then
161
+ expect(actual).to include(h1: 'H1', h2: 'H2B', h3: 'H3A')
162
+ end
163
+
164
+ it 'works even if heading not on the same level' do
165
+ # Note: The 6th <p> is inside a div
166
+ # Given
167
+ nodes = hierarchy_page_file.html_nodes
168
+ p = nodes[5]
169
+
170
+ # When
171
+ actual = hierarchy_page_file.node_hierarchy(p)
172
+
173
+ # Then
174
+ expect(actual).to include(h1: 'H1', h2: 'H2B', h3: 'H3A', h4: 'H4')
175
+ end
176
+
177
+ it 'includes node in the output if headings are indexed' do
178
+ # Given
179
+ site = get_site(algolia: { 'record_css_selector' => 'h1' })
180
+ hierarchy_page_file = extractor.new(site.file_by_name('hierarchy.md'))
181
+ nodes = hierarchy_page_file.html_nodes
182
+ h1 = nodes[0]
183
+
184
+ # When
185
+ actual = hierarchy_page_file.node_hierarchy(h1)
186
+
187
+ # Then
188
+ expect(actual).to include(h1: 'H1')
189
+ end
190
+
191
+ it 'escape html in headings' do
192
+ # Given
193
+ nodes = hierarchy_page_file.html_nodes
194
+ p = nodes[7]
195
+
196
+ # When
197
+ actual = hierarchy_page_file.node_hierarchy(p)
198
+
199
+ # Then
200
+ expect(actual).to include(h3: 'H3B &lt;code&gt;')
201
+ end
202
+ end
203
+
204
+ describe 'node_raw_html' do
205
+ it 'returns html including surrounding tags' do
206
+ # Note: 3rd <p> is a real HTML with a custom class
207
+ # Given
208
+ nodes = page_file.html_nodes
209
+ p = nodes[3]
210
+
211
+ # When
212
+ actual = page_file.node_raw_html(p)
213
+
214
+ # Then
215
+ expect(actual).to eq '<p id="text4">Another text 4</p>'
216
+ end
217
+ end
218
+
219
+ describe 'node_text' do
220
+ it 'returns inner text with <> escaped' do
221
+ # Note: 4th <p> contains a <code> tag with <>
222
+ # Given
223
+ nodes = page_file.html_nodes
224
+ p = nodes[4]
225
+
226
+ # When
227
+ actual = page_file.node_text(p)
228
+
229
+ # Then
230
+ expect(actual).to eq 'Another &lt;text&gt; 5'
231
+ end
232
+ end
233
+
234
+ describe 'unique_hierarchy' do
235
+ it 'combines title and headings' do
236
+ # Given
237
+ hierarchy = {
238
+ title: 'title',
239
+ h1: 'h1',
240
+ h2: 'h2',
241
+ h3: 'h3',
242
+ h4: 'h4',
243
+ h5: 'h5',
244
+ h6: 'h6'
245
+ }
246
+
247
+ # When
248
+ actual = page_file.unique_hierarchy(hierarchy)
249
+
250
+ # Then
251
+ expect(actual).to eq 'title > h1 > h2 > h3 > h4 > h5 > h6'
252
+ end
253
+
254
+ it 'combines title and headings even with missing elements' do
255
+ # Given
256
+ hierarchy = {
257
+ title: 'title',
258
+ h2: 'h2',
259
+ h4: 'h4',
260
+ h6: 'h6'
261
+ }
262
+
263
+ # When
264
+ actual = page_file.unique_hierarchy(hierarchy)
265
+
266
+ # Then
267
+ expect(actual).to eq 'title > h2 > h4 > h6'
268
+ end
269
+ end
270
+
271
+ describe 'node_css_selector' do
272
+ it 'uses the #id to make the selector more precise if one is found' do
273
+ # Given
274
+ nodes = page_file.html_nodes
275
+ p = nodes[3]
276
+
277
+ # When
278
+ actual = page_file.node_css_selector(p)
279
+
280
+ # Then
281
+ expect(actual).to eq '#text4'
282
+ end
283
+
284
+ it 'uses p:nth-of-type if no #id found' do
285
+ # Given
286
+ nodes = page_file.html_nodes
287
+ p = nodes[2]
288
+
289
+ # When
290
+ actual = page_file.node_css_selector(p)
291
+
292
+ # Then
293
+ expect(actual).to eq 'p:nth-of-type(3)'
294
+ end
295
+
296
+ it 'handles custom <div> markup' do
297
+ # Given
298
+ nodes = page_file.html_nodes
299
+ p = nodes[5]
300
+
301
+ # When
302
+ actual = page_file.node_css_selector(p)
303
+
304
+ # Then
305
+ expect(actual).to eq 'div:nth-of-type(2) > p'
306
+ end
307
+ end
308
+
309
+ describe 'weight' do
310
+ it 'gets the number of words in text also in the title' do
311
+ # Given
312
+ data = {
313
+ title: 'foo bar',
314
+ text: 'Lorem ipsum dolor foo bar, consectetur adipiscing elit'
315
+ }
316
+
317
+ # When
318
+ actual = page_file.weight(data)
319
+
320
+ # Then
321
+ expect(actual).to eq 2
322
+ end
323
+
324
+ it 'gets the number of words in text also in the headings' do
325
+ # Given
326
+ data = {
327
+ title: 'foo',
328
+ h1: 'bar',
329
+ h2: 'baz',
330
+ text: 'Lorem baz dolor foo bar, consectetur adipiscing elit'
331
+ }
332
+
333
+ # When
334
+ actual = page_file.weight(data)
335
+
336
+ # Then
337
+ expect(actual).to eq 3
338
+ end
339
+
340
+ it 'count each word only once' do
341
+ # Given
342
+ data = {
343
+ title: 'foo',
344
+ h1: 'foo foo foo',
345
+ h2: 'bar bar foo bar',
346
+ text: 'foo bar bar bar bar baz foo bar baz'
347
+ }
348
+
349
+ # When
350
+ actual = page_file.weight(data)
351
+
352
+ # Then
353
+ expect(actual).to eq 2
354
+ end
355
+
356
+ it 'is case-insensitive' do
357
+ # Given
358
+ data = {
359
+ title: 'FOO',
360
+ h1: 'bar Bar BAR',
361
+ text: 'foo BAR'
362
+ }
363
+
364
+ # When
365
+ actual = page_file.weight(data)
366
+
367
+ # Then
368
+ expect(actual).to eq 2
369
+ end
370
+
371
+ it 'should only use words, no partial matches' do
372
+ # Given
373
+ data = {
374
+ title: 'foo bar',
375
+ text: 'xxxfooxxx bar'
376
+ }
377
+
378
+ # When
379
+ actual = page_file.weight(data)
380
+
381
+ # Then
382
+ expect(actual).to eq 1
383
+
384
+ # I don't seem fit for that
385
+ end
386
+
387
+ it 'should still work with non-string keys' do
388
+ # Given
389
+ data = {
390
+ title: nil,
391
+ h1: [],
392
+ h2: {},
393
+ h3: true,
394
+ h4: false,
395
+ h5: 'foo bar',
396
+ text: 'foo bar'
397
+ }
398
+
399
+ # When
400
+ actual = page_file.weight(data)
401
+
402
+ # Then
403
+ expect(actual).to eq 2
404
+ end
405
+ end
406
+
407
+ describe 'custom_hook_each' do
408
+ it 'let the user call a custom hook to modify a record' do
409
+ # Given
410
+ def page_file.custom_hook_each(item, _)
411
+ item[:custom_attribute] = 'foo'
412
+ item
413
+ end
414
+
415
+ # When
416
+ actual = page_file.extract
417
+
418
+ # Then
419
+ expect(actual[0]).to include(custom_attribute: 'foo')
420
+ end
421
+
422
+ it 'let the user discard a record by returning nil' do
423
+ # Given
424
+ def page_file.custom_hook_each(_, _)
425
+ nil
426
+ end
427
+
428
+ # When
429
+ actual = page_file.extract
430
+
431
+ # Then
432
+ expect(actual.size).to eq 0
433
+ end
434
+ end
435
+
436
+ describe 'custom_hook_all' do
437
+ it 'let the user call a custom hook to modify the list of records' do
438
+ # Given
439
+ def page_file.custom_hook_all(items)
440
+ [items[0], { foo: 'bar' }]
441
+ end
442
+
443
+ # When
444
+ actual = page_file.extract
445
+
446
+ # Then
447
+ expect(actual.size).to eq 2
448
+ expect(actual[1]).to include(foo: 'bar')
449
+ end
450
+ end
451
+ end
@@ -0,0 +1,33 @@
1
+ require 'awesome_print'
2
+ require_relative './spec_helper_jekyll.rb'
3
+ require_relative './spec_helper_simplecov.rb'
4
+ require './lib/push.rb'
5
+
6
+ RSpec.configure do |config|
7
+ config.filter_run(focus: true)
8
+ config.run_all_when_everything_filtered = true
9
+
10
+ # Build a jekyll site, creating access to @__files used internally
11
+ def get_site(config = {})
12
+ config = config.merge(
13
+ source: File.expand_path('./spec/fixtures')
14
+ )
15
+ config = Jekyll.configuration(config)
16
+ site = Jekyll::Site.new(config)
17
+
18
+ # Keep a list of all files
19
+ def site.write
20
+ @__files = {}
21
+ each_site_file do |file|
22
+ @__files[file.path] = file
23
+ end
24
+ end
25
+
26
+ def site.file_by_name(file_name)
27
+ @__files.find { |path, _| path =~ /#{file_name}$/ }[1]
28
+ end
29
+
30
+ site.process
31
+ site
32
+ end
33
+ end
@@ -0,0 +1,4 @@
1
+ require 'jekyll'
2
+
3
+ # Prevent Jekyll from displaying the "Configuration file:..." on every test
4
+ Jekyll.logger.log_level = :error
@@ -0,0 +1,9 @@
1
+ require 'simplecov'
2
+
3
+ SimpleCov.configure do
4
+ load_adapter 'test_frameworks'
5
+ end
6
+
7
+ ENV['COVERAGE'] && SimpleCov.start do
8
+ add_filter '/.rvm/'
9
+ end