algoliasearch-jekyll 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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