crawlscope 0.7.2 → 0.8.1

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/README.md +95 -18
  4. data/lib/crawlscope/browser.rb +18 -1
  5. data/lib/crawlscope/configuration.rb +6 -1
  6. data/lib/crawlscope/crawl.rb +5 -3
  7. data/lib/crawlscope/http.rb +13 -2
  8. data/lib/crawlscope/page.rb +8 -0
  9. data/lib/crawlscope/reporter.rb +144 -61
  10. data/lib/crawlscope/request_headers.rb +34 -0
  11. data/lib/crawlscope/result.rb +4 -0
  12. data/lib/crawlscope/rules/content_quality.rb +5 -5
  13. data/lib/crawlscope/rules/indexability.rb +1 -1
  14. data/lib/crawlscope/rules/links.rb +28 -34
  15. data/lib/crawlscope/rules/metadata.rb +25 -25
  16. data/lib/crawlscope/rules/structured_data.rb +13 -4
  17. data/lib/crawlscope/rules/uniqueness.rb +9 -7
  18. data/lib/crawlscope/run.rb +0 -10
  19. data/lib/crawlscope/schema_registry.rb +17 -1
  20. data/lib/crawlscope/server_timing/reporter.rb +114 -0
  21. data/lib/crawlscope/server_timing/summary.rb +107 -0
  22. data/lib/crawlscope/server_timing.rb +138 -0
  23. data/lib/crawlscope/sitemap.rb +17 -5
  24. data/lib/crawlscope/structured_data/error_formatter.rb +42 -0
  25. data/lib/crawlscope/structured_data/reporter.rb +10 -10
  26. data/lib/crawlscope/version.rb +1 -1
  27. data/lib/generators/crawlscope/templates/initializer.rb.tt +1 -1
  28. data/test/crawlscope/browser_test.rb +56 -3
  29. data/test/crawlscope/configuration_test.rb +50 -10
  30. data/test/crawlscope/crawl_test.rb +65 -0
  31. data/test/crawlscope/http_test.rb +60 -1
  32. data/test/crawlscope/install_generator_test.rb +9 -11
  33. data/test/crawlscope/links_rule_test.rb +40 -4
  34. data/test/crawlscope/metadata_rule_test.rb +37 -4
  35. data/test/crawlscope/reporter_test.rb +249 -20
  36. data/test/crawlscope/run_test.rb +4 -12
  37. data/test/crawlscope/schema_registry_test.rb +33 -0
  38. data/test/crawlscope/server_timing_summary_test.rb +87 -0
  39. data/test/crawlscope/server_timing_test.rb +101 -0
  40. data/test/crawlscope/sitemap_test.rb +21 -0
  41. data/test/crawlscope/structured_data_error_formatter_test.rb +46 -0
  42. data/test/crawlscope/structured_data_reporter_test.rb +26 -0
  43. data/test/crawlscope/structured_data_rule_test.rb +2 -0
  44. data/test/crawlscope/uniqueness_rule_test.rb +46 -3
  45. metadata +10 -2
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ class CrawlscopeStructuredDataErrorFormatterTest < Minitest::Test
6
+ def test_formats_missing_root_and_nested_fields_from_real_schema_errors
7
+ registry = Crawlscope::SchemaRegistry.default
8
+ errors = registry.validate({"@type" => "WebApplication", "name" => "App", "review" => {"@type" => "Review"}})
9
+
10
+ assert_equal "WebApplication.review/reviewRating: is required", format_error(errors.first)
11
+ assert_equal "Article.headline: is required", format_error(registry.validate({"@type" => "Article"}).first)
12
+ end
13
+
14
+ def test_preserves_unknown_constraints_without_schema_noise
15
+ error = {"field" => "price", "type" => "Product", "issue" => "The property '#/price' must satisfy a custom constraint in schema 3bdea3a8-461f-5eae-9d73-3243b48b4bc8"}
16
+
17
+ assert_equal "Product.price: must satisfy a custom constraint", format_error(error)
18
+ assert_equal "$: Unknown error", format_error({})
19
+ end
20
+
21
+ def test_formats_custom_minimum_length_constraint
22
+ registry = Crawlscope::SchemaRegistry.new(schemas: {"Thing" => {type: "object", properties: {name: {type: "string", minLength: 3}}}})
23
+ error = registry.validate({"@type" => "Thing", "name" => "x"}).first
24
+
25
+ assert_equal "Thing.name: 1 character; minimum 3", format_error(error)
26
+ end
27
+
28
+ def test_formats_measured_headline_length_with_symbol_or_json_keys
29
+ error = Crawlscope::SchemaRegistry.default.validate({"@type" => "Article", "headline" => "é" * 111}).first
30
+
31
+ assert_equal "Article.headline: 111 characters; maximum 110", format_error(error)
32
+ assert_equal "Article.headline: 111 characters; maximum 110", format_error(JSON.parse(JSON.generate(error)))
33
+ end
34
+
35
+ def test_formats_legacy_length_errors_without_a_measurement
36
+ error = {type: "Article", field: "headline", issue: "The property '#/headline' was not of a maximum string length of 110"}
37
+
38
+ assert_equal "Article.headline: exceeds 110 characters", format_error(error)
39
+ end
40
+
41
+ private
42
+
43
+ def format_error(error)
44
+ Crawlscope::StructuredData::ErrorFormatter.format(error)
45
+ end
46
+ end
@@ -29,4 +29,30 @@ class CrawlscopeStructuredDataReporterTest < Minitest::Test
29
29
  assert_includes output, "headline: is required"
30
30
  assert_includes output, "/tmp/structured_data_report.json"
31
31
  end
32
+
33
+ def test_formats_real_schema_errors_in_summary_and_detailed_reports
34
+ errors = Crawlscope::SchemaRegistry.default.validate({"@type" => "Article", "headline" => "x" * 111})
35
+ result = Crawlscope::StructuredData::Audit::Outcome.new(entries: [
36
+ Crawlscope::StructuredData::Audit::Page.new(
37
+ url: "https://example.com/article", status: 200,
38
+ structured_items: [{source: "json-ld", data: {"@type" => "Article"}}],
39
+ errors: [{type: "Article", source: "json-ld", errors: errors}],
40
+ fetch_error: nil, content_type: "text/html", skipped_reason: nil
41
+ )
42
+ ])
43
+ io = StringIO.new
44
+ reporter = Crawlscope::StructuredData::Reporter.new(io: io)
45
+
46
+ reporter.report(result)
47
+ assert_includes io.string, "JSON-LD:\n - Article.headline: 111 characters; maximum 110"
48
+ refute_includes io.string, "in schema"
49
+
50
+ io.truncate(0)
51
+ io.rewind
52
+ reporter.details(result, debug: false, renderer: :http)
53
+ assert_includes io.string, "Validation: FAILED\n JSON-LD:\n - Article.headline: 111 characters; maximum 110"
54
+ refute_includes io.string, "=" * 80
55
+ refute_includes io.string, "in schema"
56
+ assert_includes errors.first[:issue], "in schema"
57
+ end
32
58
  end
@@ -31,6 +31,8 @@ class CrawlscopeStructuredDataRuleTest < Minitest::Test
31
31
 
32
32
  assert_equal [:structured_data_schema_error], issues.to_a.map(&:code)
33
33
  assert_includes issues.to_a.first.message, "headline"
34
+ assert_equal "JSON-LD Article.headline: is required", issues.to_a.first.message
35
+ assert_includes issues.to_a.first.details[:errors].first[:issue], "in schema"
34
36
  end
35
37
 
36
38
  def test_reports_parse_errors_for_invalid_json_ld
@@ -44,6 +44,49 @@ class CrawlscopeUniquenessRuleTest < Minitest::Test
44
44
  assert_operator issue.details[:similarity], :>=, issue.details[:threshold]
45
45
  end
46
46
 
47
+ def test_does_not_report_redirect_aliases_as_duplicate_pages
48
+ pages = [
49
+ page(url: "https://example.com/old", final_url: "https://example.com/current"),
50
+ page(url: "https://example.com/alias", final_url: "https://example.com/current"),
51
+ page(url: "https://example.com/current")
52
+ ]
53
+ issues = Crawlscope::IssueCollection.new
54
+
55
+ Crawlscope::Rules::Uniqueness.new.call(urls: pages.map(&:url), pages: pages, issues: issues, context: {})
56
+
57
+ assert_empty issues.to_a
58
+ end
59
+
60
+ def test_reports_direct_duplicate_pages_without_including_redirect_aliases
61
+ pages = [
62
+ page(url: "https://example.com/old", final_url: "https://example.com/a"),
63
+ page(url: "https://example.com/a"),
64
+ page(url: "https://example.com/b")
65
+ ]
66
+ issues = Crawlscope::IssueCollection.new
67
+
68
+ Crawlscope::Rules::Uniqueness.new.call(urls: pages.map(&:url), pages: pages, issues: issues, context: {})
69
+
70
+ assert_equal 4, issues.size
71
+ issues.each do |issue|
72
+ assert_equal ["https://example.com/a", "https://example.com/b"], issue.details[:urls]
73
+ end
74
+ end
75
+
76
+ def test_excludes_redirects_from_near_duplicate_checks_and_scan_limit
77
+ pages = [
78
+ page(url: "https://example.com/old", final_url: "https://example.com/other", content: near_duplicate_content("reliable")),
79
+ page(url: "https://example.com/current", content: near_duplicate_content("dependable"))
80
+ ]
81
+
82
+ [Crawlscope::Rules::Uniqueness.new, Crawlscope::Rules::Uniqueness.new(max_near_duplicate_pages: 1)].each do |rule|
83
+ issues = Crawlscope::IssueCollection.new
84
+ rule.call(urls: pages.map(&:url), pages: pages, issues: issues, context: {})
85
+
86
+ assert_empty issues.to_a
87
+ end
88
+ end
89
+
47
90
  def test_skips_near_duplicate_scan_when_page_count_exceeds_limit
48
91
  issues = Crawlscope::IssueCollection.new
49
92
  rule = Crawlscope::Rules::Uniqueness.new(max_near_duplicate_pages: 1)
@@ -72,7 +115,7 @@ class CrawlscopeUniquenessRuleTest < Minitest::Test
72
115
  TEXT
73
116
  end
74
117
 
75
- def page(url:, content: nil, canonical: nil)
118
+ def page(url:, content: nil, canonical: nil, final_url: url)
76
119
  repeated_text = content || ("Useful content " * 30).strip
77
120
  canonical_tag = canonical ? %(<link rel="canonical" href="#{canonical}">) : ""
78
121
  body = <<~HTML
@@ -91,8 +134,8 @@ class CrawlscopeUniquenessRuleTest < Minitest::Test
91
134
  Crawlscope::Page.new(
92
135
  url: url,
93
136
  normalized_url: url,
94
- final_url: url,
95
- normalized_final_url: url,
137
+ final_url: final_url,
138
+ normalized_final_url: final_url,
96
139
  status: 200,
97
140
  headers: {"content-type" => "text/html"},
98
141
  body: body,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: crawlscope
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Fidalgo
@@ -238,6 +238,7 @@ files:
238
238
  - lib/crawlscope/railtie.rb
239
239
  - lib/crawlscope/rake_tasks.rb
240
240
  - lib/crawlscope/reporter.rb
241
+ - lib/crawlscope/request_headers.rb
241
242
  - lib/crawlscope/result.rb
242
243
  - lib/crawlscope/rule_registry.rb
243
244
  - lib/crawlscope/rules/content_quality.rb
@@ -249,10 +250,14 @@ files:
249
250
  - lib/crawlscope/run.rb
250
251
  - lib/crawlscope/schema_registry.rb
251
252
  - lib/crawlscope/schemas.rb
253
+ - lib/crawlscope/server_timing.rb
254
+ - lib/crawlscope/server_timing/reporter.rb
255
+ - lib/crawlscope/server_timing/summary.rb
252
256
  - lib/crawlscope/sitemap.rb
253
257
  - lib/crawlscope/structured_data/audit.rb
254
258
  - lib/crawlscope/structured_data/check.rb
255
259
  - lib/crawlscope/structured_data/document.rb
260
+ - lib/crawlscope/structured_data/error_formatter.rb
256
261
  - lib/crawlscope/structured_data/report.rb
257
262
  - lib/crawlscope/structured_data/reporter.rb
258
263
  - lib/crawlscope/structured_data/writer.rb
@@ -281,9 +286,12 @@ files:
281
286
  - test/crawlscope/rule_registry_test.rb
282
287
  - test/crawlscope/run_test.rb
283
288
  - test/crawlscope/schema_registry_test.rb
289
+ - test/crawlscope/server_timing_summary_test.rb
290
+ - test/crawlscope/server_timing_test.rb
284
291
  - test/crawlscope/sitemap_test.rb
285
292
  - test/crawlscope/structured_data_audit_test.rb
286
293
  - test/crawlscope/structured_data_document_test.rb
294
+ - test/crawlscope/structured_data_error_formatter_test.rb
287
295
  - test/crawlscope/structured_data_report_test.rb
288
296
  - test/crawlscope/structured_data_reporter_test.rb
289
297
  - test/crawlscope/structured_data_rule_test.rb
@@ -322,7 +330,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
322
330
  - !ruby/object:Gem::Version
323
331
  version: '0'
324
332
  requirements: []
325
- rubygems_version: 4.0.16
333
+ rubygems_version: 4.0.20
326
334
  specification_version: 4
327
335
  summary: Audit sitemap URLs for metadata, structured data, uniqueness, and links
328
336
  test_files: []