History

Update minitest to v6. Add coverage reporter.

Fix frozen literal warning; add tests.

spamguy committed 7 months ago

Commit b55623b1 parent c8e8f3216 files · +1102 −55Browse files

Unified · Split

16 files
  1. .gitignore +1 −0
  2. Gemfile +1 −0
  3. Gemfile.lock +12 −1
  4. README.md +52 −51
  5. lib/docs/filters/core/clean_text.rb +6 −3
  6. lib/tasks/test.thor +7 −0
  7. test/lib/docs/core/doc_test.rb +393 −0
  8. test/lib/docs/core/entry_index_test.rb +66 −0
  9. test/lib/docs/core/filter_test.rb +38 −0
  10. test/lib/docs/core/scraper_test.rb +162 −0
  11. test/lib/docs/core/scrapers/url_scraper_test.rb +62 −0
  12. test/lib/docs/core/subscriber_test.rb +98 −0
  13. test/lib/docs/filters/core/attribution_test.rb +39 −0
  14. test/lib/docs/filters/core/clean_local_urls_test.rb +22 −0
  15. test/lib/docs/filters/core/images_test.rb +133 −0
  16. test/test_helper.rb +10 −0
.gitignore+1−0
@@ -8,6 +8,7 @@ public/docs/**/* · 7 unmodified lines
8docs/**/*8docs/**/*
9!docs/*.md9!docs/*.md
10/vendor10/vendor
11/coverage
11*.tar12*.tar
12*.tar.bz213*.tar.bz2
13*.tar.gz14*.tar.gz
Gemfile+1−0
@@ -48,6 +48,7 @@ group :test do · 47 unmodified lines
48 gem 'minitest'48 gem 'minitest'
49 gem 'rack-test', require: false49 gem 'rack-test', require: false
50 gem 'rr', require: false50 gem 'rr', require: false
51 gem 'simplecov', require: false
51end52end
52 53
53if ENV['SELENIUM'] == '1'54if ENV['SELENIUM'] == '1'
Gemfile.lock+12−1
@@ -56,6 +56,7 @@ GEM · 55 unmodified lines
56 sprockets-rails56 sprockets-rails
57 tilt57 tilt
58 date (3.5.1)58 date (3.5.1)
59 docile (1.4.1)
59 drb (2.2.3)60 drb (2.2.3)
60 erb (6.0.1)61 erb (6.0.1)
61 erubi (1.13.1)62 erubi (1.13.1)
@@ -99,7 +100,9 @@ GEM · 37 unmodified lines
99 nokogiri (>= 1.12.0)100 nokogiri (>= 1.12.0)
100 method_source (1.1.0)101 method_source (1.1.0)
101 mini_portile2 (2.8.9)102 mini_portile2 (2.8.9)
102 minitest (5.27.0)103 minitest (6.0.2)
104 drb (~> 2.0)
105 prism (~> 1.5)
103 multi_json (1.15.0)106 multi_json (1.15.0)
104 mustermann (3.0.3)107 mustermann (3.0.3)
105 ruby2_keywords (~> 0.0.1)108 ruby2_keywords (~> 0.0.1)
@@ -111,6 +114,7 @@ GEM · 5 unmodified lines
111 pp (0.6.3)114 pp (0.6.3)
112 prettyprint115 prettyprint
113 prettyprint (0.2.0)116 prettyprint (0.2.0)
117 prism (1.9.0)
114 progress (3.6.0)118 progress (3.6.0)
115 progress_bar (1.3.4)119 progress_bar (1.3.4)
116 highline (>= 1.6)120 highline (>= 1.6)
@@ -174,6 +178,12 @@ GEM · 57 unmodified lines
174 sassc-embedded (1.80.8)178 sassc-embedded (1.80.8)
175 sass-embedded (~> 1.80)179 sass-embedded (~> 1.80)
176 securerandom (0.3.2)180 securerandom (0.3.2)
181 simplecov (0.22.0)
182 docile (~> 1.1)
183 simplecov-html (~> 0.11)
184 simplecov_json_formatter (~> 0.1)
185 simplecov-html (0.13.2)
186 simplecov_json_formatter (0.1.4)
177 sinatra (3.2.0)187 sinatra (3.2.0)
178 mustermann (~> 3.0)188 mustermann (~> 3.0)
179 rack (~> 2.2, >= 2.2.4)189 rack (~> 2.2, >= 2.2.4)
@@ -253,6 +263,7 @@ DEPENDENCIES · 73 unmodified lines
253 redcarpet263 redcarpet
254 rr264 rr
255 rss265 rss
266 simplecov
256 sinatra267 sinatra
257 sinatra-contrib268 sinatra-contrib
258 sprockets269 sprockets
README.md+52−51
@@ -10,9 +10,9 @@ Please reach out to the community on [Discord](https://discord.gg/PRyKn3Vbay) if · 9 unmodified lines
10 10
11Keep track of development news:11Keep track of development news:
12 12
13* Join the `#contributors` chat room on [Discord](https://discord.gg/PRyKn3Vbay)13- Join the `#contributors` chat room on [Discord](https://discord.gg/PRyKn3Vbay)
14* Watch the repository on [GitHub](https://github.com/freeCodeCamp/devdocs/subscription)14- Watch the repository on [GitHub](https://github.com/freeCodeCamp/devdocs/subscription)
15* Follow [@DevDocs](https://twitter.com/DevDocs) on Twitter15- Follow [@DevDocs](https://twitter.com/DevDocs) on Twitter
16 16
17**Table of Contents:** [Quick Start](#quick-start) · [Vision](#vision) · [App](#app) · [Scraper](#scraper) · [Commands](#available-commands) · [Contributing](#contributing) · [Documentation](#documentation) · [Related Projects](#related-projects) · [License](#copyright--license) · [Questions?](#questions)17**Table of Contents:** [Quick Start](#quick-start) · [Vision](#vision) · [App](#app) · [Scraper](#scraper) · [Commands](#available-commands) · [Contributing](#contributing) · [Documentation](#documentation) · [Related Projects](#related-projects) · [License](#copyright--license) · [Questions?](#questions)
18 18
@@ -29,6 +29,7 @@ docker run --name devdocs -d -p 9292:9292 ghcr.io/freecodecamp/devdocs:latest · 10 unmodified lines
29```29```
30 30
31This will start DevDocs at [localhost:9292](http://localhost:9292). We provide both regular and Alpine-based images:31This will start DevDocs at [localhost:9292](http://localhost:9292). We provide both regular and Alpine-based images:
32
32- `ghcr.io/freecodecamp/devdocs:latest` - Standard image33- `ghcr.io/freecodecamp/devdocs:latest` - Standard image
33- `ghcr.io/freecodecamp/devdocs:latest-alpine` - Alpine-based (smaller size)34- `ghcr.io/freecodecamp/devdocs:latest-alpine` - Alpine-based (smaller size)
34 35
@@ -70,14 +71,14 @@ DevDocs aims to make reading and searching reference documentation fast, easy an · 35 unmodified lines
70 71
71The app's main goals are to:72The app's main goals are to:
72 73
73* Keep load times as short as possible74- Keep load times as short as possible
74* Improve the quality, speed, and order of search results75- Improve the quality, speed, and order of search results
75* Maximize the use of caching and other performance optimizations76- Maximize the use of caching and other performance optimizations
76* Maintain a clean and readable user interface77- Maintain a clean and readable user interface
77* Be fully functional offline78- Be fully functional offline
78* Support full keyboard navigation79- Support full keyboard navigation
79* Reduce “context switch” by using a consistent typography and design across all documentations80- Reduce “context switch” by using a consistent typography and design across all documentations
80* Reduce clutter by focusing on a specific category of content (API/reference) and indexing only the minimum useful to most developers.81- Reduce clutter by focusing on a specific category of content (API/reference) and indexing only the minimum useful to most developers.
81 82
82**Note:** DevDocs is neither a programming guide nor a search engine. All our content is pulled from third-party sources and the project doesn't intend to compete with full-text search engines. Its backbone is metadata; each piece of content is identified by a unique, "obvious" and short string. Tutorials, guides and other content that don't meet this requirement are outside the scope of the project.83**Note:** DevDocs is neither a programming guide nor a search engine. All our content is pulled from third-party sources and the project doesn't intend to compete with full-text search engines. Its backbone is metadata; each piece of content is identified by a unique, "obvious" and short string. Tutorials, guides and other content that don't meet this requirement are outside the scope of the project.
83 84
@@ -91,10 +92,10 @@ Another driving factor is performance and the fact that everything happens in th · 7 unmodified lines
91 92
92DevDocs being a developer tool, the browser requirements are high:93DevDocs being a developer tool, the browser requirements are high:
93 94
94* Recent versions of Firefox, Chrome, or Opera95- Recent versions of Firefox, Chrome, or Opera
95* Safari 11.1+96- Safari 11.1+
96* Edge 17+97- Edge 17+
97* iOS 11.3+98- iOS 11.3+
98 99
99This allows the code to take advantage of the latest DOM and HTML5 APIs and make developing DevDocs a lot more fun!100This allows the code to take advantage of the latest DOM and HTML5 APIs and make developing DevDocs a lot more fun!
100 101
@@ -106,12 +107,12 @@ There are currently two kinds of scrapers: `UrlScraper` which downloads files vi · 5 unmodified lines
106 107
107Modifications made to each document include:108Modifications made to each document include:
108 109
109* removing content such as the document structure (`<html>`, `<head>`, etc.), comments, empty nodes, etc.110- removing content such as the document structure (`<html>`, `<head>`, etc.), comments, empty nodes, etc.
110* fixing links (e.g. to remove duplicates)111- fixing links (e.g. to remove duplicates)
111* replacing all external (not scraped) URLs with their fully qualified counterpart112- replacing all external (not scraped) URLs with their fully qualified counterpart
112* replacing all internal (scraped) URLs with their unqualified and relative counterpart113- replacing all internal (scraped) URLs with their unqualified and relative counterpart
113* adding content, such as a title and link to the original document114- adding content, such as a title and link to the original document
114* ensuring correct syntax highlighting using [Prism](http://prismjs.com/)115- ensuring correct syntax highlighting using [Prism](http://prismjs.com/)
115 116
116These modifications are applied via a set of filters using the [HTML::Pipeline](https://github.com/jch/html-pipeline) library. Each scraper includes filters specific to itself, one of which is tasked with figuring out the pages' metadata.117These modifications are applied via a set of filters using the [HTML::Pipeline](https://github.com/jch/html-pipeline) library. Each scraper includes filters specific to itself, one of which is tasked with figuring out the pages' metadata.
117 118
@@ -143,9 +144,10 @@ thor console:docs # Start a REPL in the "Docs" module · 25 unmodified lines
143 144
144# Tests can be run quickly from within the console using the "test" command.145# Tests can be run quickly from within the console using the "test" command.
145# Run "help test" for usage instructions.146# Run "help test" for usage instructions.
146thor test:all # Run all tests147thor test:all # Run all tests
147thor test:docs # Run "Docs" tests148thor test:docs # Run "Docs" tests
148thor test:app # Run "App" tests149thor test:app # Run "App" tests
150thor test:app:coverage # Run coverage for "App" tests
149 151
150# Assets152# Assets
151thor assets:compile # Compile assets (not required in development mode)153thor assets:compile # Compile assets (not required in development mode)
@@ -160,10 +162,10 @@ Contributions are welcome. Please read the [contributing guidelines](./.github/C · 8 unmodified lines
160 162
161## Documentation163## Documentation
162 164
163* [Adding documentations to DevDocs](./docs/adding-docs.md)165- [Adding documentations to DevDocs](./docs/adding-docs.md)
164* [Scraper Reference](./docs/scraper-reference.md)166- [Scraper Reference](./docs/scraper-reference.md)
165* [Filter Reference](./docs/filter-reference.md)167- [Filter Reference](./docs/filter-reference.md)
166* [Maintainers’ Guide](./docs/maintainers.md)168- [Maintainers’ Guide](./docs/maintainers.md)
167 169
168## DevDocs Quick Usage Cheatsheet170## DevDocs Quick Usage Cheatsheet
169 171
@@ -182,35 +184,34 @@ Below are some helpful shortcuts and usage tips that are not immediately obvious · 12 unmodified lines
182 184
183These shortcuts make DevDocs faster to navigate and more efficient for daily use.185These shortcuts make DevDocs faster to navigate and more efficient for daily use.
184 186
185
186## Related Projects187## Related Projects
187 188
188Made something cool? Feel free to open a PR to add a new row to this table! You might want to discover new projects via https://github.com/topics/devdocs.189Made something cool? Feel free to open a PR to add a new row to this table! You might want to discover new projects via https://github.com/topics/devdocs.
189 190
190<!-- table is sorted by description -->191<!-- table is sorted by description -->
191 192
192| Project | Description | Last commit | Stars |193| Project | Description | Last commit | Stars |
193| ------------------------------------------------------------------------------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |194| ------------------------------------------------------------------------------------------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
194| [yannickglt/alfred-devdocs](https://github.com/yannickglt/alfred-devdocs) | Alfred workflow | ![Latest GitHub commit](https://img.shields.io/github/last-commit/yannickglt/alfred-devdocs?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/yannickglt/alfred-devdocs?logo=github&label) |195| [yannickglt/alfred-devdocs](https://github.com/yannickglt/alfred-devdocs) | Alfred workflow | ![Latest GitHub commit](https://img.shields.io/github/last-commit/yannickglt/alfred-devdocs?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/yannickglt/alfred-devdocs?logo=github&label) |
195| [Merith-TK/devdocs_webapp_kotlin](https://github.com/Merith-TK/devdocs_webapp_kotlin) | Android application | ![Latest GitHub commit](https://img.shields.io/github/last-commit/Merith-TK/devdocs_webapp_kotlin?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/Merith-TK/devdocs_webapp_kotlin?logo=github&label) |196| [Merith-TK/devdocs_webapp_kotlin](https://github.com/Merith-TK/devdocs_webapp_kotlin) | Android application | ![Latest GitHub commit](https://img.shields.io/github/last-commit/Merith-TK/devdocs_webapp_kotlin?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/Merith-TK/devdocs_webapp_kotlin?logo=github&label) |
196| [gruehle/dev-docs-viewer](https://github.com/gruehle/dev-docs-viewer) | Brackets extension | ![Latest GitHub commit](https://img.shields.io/github/last-commit/gruehle/dev-docs-viewer?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/gruehle/dev-docs-viewer?logo=github&label) |197| [gruehle/dev-docs-viewer](https://github.com/gruehle/dev-docs-viewer) | Brackets extension | ![Latest GitHub commit](https://img.shields.io/github/last-commit/gruehle/dev-docs-viewer?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/gruehle/dev-docs-viewer?logo=github&label) |
197| [egoist/devdocs-desktop](https://github.com/egoist/devdocs-desktop) | Electron application | ![Latest GitHub commit](https://img.shields.io/github/last-commit/egoist/devdocs-desktop?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/egoist/devdocs-desktop?logo=github&label) |198| [egoist/devdocs-desktop](https://github.com/egoist/devdocs-desktop) | Electron application | ![Latest GitHub commit](https://img.shields.io/github/last-commit/egoist/devdocs-desktop?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/egoist/devdocs-desktop?logo=github&label) |
198| [skeeto/devdocs-lookup](https://github.com/skeeto/devdocs-lookup) | Emacs function | ![Latest GitHub commit](https://img.shields.io/github/last-commit/skeeto/devdocs-lookup?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/skeeto/devdocs-lookup?logo=github&label) |199| [skeeto/devdocs-lookup](https://github.com/skeeto/devdocs-lookup) | Emacs function | ![Latest GitHub commit](https://img.shields.io/github/last-commit/skeeto/devdocs-lookup?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/skeeto/devdocs-lookup?logo=github&label) |
199| [astoff/devdocs.el](https://github.com/astoff/devdocs.el) | Emacs viewer | ![Latest GitHub commit](https://img.shields.io/github/last-commit/astoff/devdocs.el?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/astoff/devdocs.el?logo=github&label) |200| [astoff/devdocs.el](https://github.com/astoff/devdocs.el) | Emacs viewer | ![Latest GitHub commit](https://img.shields.io/github/last-commit/astoff/devdocs.el?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/astoff/devdocs.el?logo=github&label) |
200| [naquad/devdocs-shell](https://github.com/naquad/devdocs-shell) | GTK shell with Vim integration | ![Latest GitHub commit](https://img.shields.io/github/last-commit/naquad/devdocs-shell?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/naquad/devdocs-shell?logo=github&label) |201| [naquad/devdocs-shell](https://github.com/naquad/devdocs-shell) | GTK shell with Vim integration | ![Latest GitHub commit](https://img.shields.io/github/last-commit/naquad/devdocs-shell?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/naquad/devdocs-shell?logo=github&label) |
201| [hardpixel/devdocs-desktop](https://github.com/hardpixel/devdocs-desktop) | GTK application | ![Latest GitHub commit](https://img.shields.io/github/last-commit/hardpixel/devdocs-desktop?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/hardpixel/devdocs-desktop?logo=github&label) |202| [hardpixel/devdocs-desktop](https://github.com/hardpixel/devdocs-desktop) | GTK application | ![Latest GitHub commit](https://img.shields.io/github/last-commit/hardpixel/devdocs-desktop?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/hardpixel/devdocs-desktop?logo=github&label) |
202| [qwfy/doc-browser](https://github.com/qwfy/doc-browser) | Linux application | ![Latest GitHub commit](https://img.shields.io/github/last-commit/qwfy/doc-browser?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/qwfy/doc-browser?logo=github&label) |203| [qwfy/doc-browser](https://github.com/qwfy/doc-browser) | Linux application | ![Latest GitHub commit](https://img.shields.io/github/last-commit/qwfy/doc-browser?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/qwfy/doc-browser?logo=github&label) |
203| [dteoh/devdocs-macos](https://github.com/dteoh/devdocs-macos) | macOS application | ![Latest GitHub commit](https://img.shields.io/github/last-commit/dteoh/devdocs-macos?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/dteoh/devdocs-macos?logo=github&label) |204| [dteoh/devdocs-macos](https://github.com/dteoh/devdocs-macos) | macOS application | ![Latest GitHub commit](https://img.shields.io/github/last-commit/dteoh/devdocs-macos?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/dteoh/devdocs-macos?logo=github&label) |
204| [Sublime Text plugin](https://sublime.wbond.net/packages/DevDocs) | Sublime Text plugin | ![Latest GitHub commit](https://img.shields.io/github/last-commit/vitorbritto/sublime-devdocs?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/vitorbritto/sublime-devdocs?logo=github&label) |205| [Sublime Text plugin](https://sublime.wbond.net/packages/DevDocs) | Sublime Text plugin | ![Latest GitHub commit](https://img.shields.io/github/last-commit/vitorbritto/sublime-devdocs?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/vitorbritto/sublime-devdocs?logo=github&label) |
205| [mohamed3nan/DevDocs-Tab](https://github.com/mohamed3nan/DevDocs-Tab) | VS Code extension (view as tab) | ![Latest GitHub commit](https://img.shields.io/github/last-commit/mohamed3nan/DevDocs-Tab?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/mohamed3nan/DevDocs-Tab?logo=github&label) |206| [mohamed3nan/DevDocs-Tab](https://github.com/mohamed3nan/DevDocs-Tab) | VS Code extension (view as tab) | ![Latest GitHub commit](https://img.shields.io/github/last-commit/mohamed3nan/DevDocs-Tab?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/mohamed3nan/DevDocs-Tab?logo=github&label) |
206| [deibit/vscode-devdocs](https://marketplace.visualstudio.com/items?itemName=deibit.devdocs) | VS Code extension (open the browser) | ![Latest GitHub commit](https://img.shields.io/github/last-commit/deibit/vscode-devdocs?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/deibit/vscode-devdocs?logo=github&label) |207| [deibit/vscode-devdocs](https://marketplace.visualstudio.com/items?itemName=deibit.devdocs) | VS Code extension (open the browser) | ![Latest GitHub commit](https://img.shields.io/github/last-commit/deibit/vscode-devdocs?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/deibit/vscode-devdocs?logo=github&label) |
207| [mdh34/quickDocs](https://github.com/mdh34/quickDocs) | Vala/Python based viewer | ![Latest GitHub commit](https://img.shields.io/github/last-commit/mdh34/quickDocs?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/mdh34/quickDocs?logo=github&label) |208| [mdh34/quickDocs](https://github.com/mdh34/quickDocs) | Vala/Python based viewer | ![Latest GitHub commit](https://img.shields.io/github/last-commit/mdh34/quickDocs?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/mdh34/quickDocs?logo=github&label) |
208| [girishji/devdocs.vim](https://github.com/girishji/devdocs.vim) | Vim plugin & TUI (browse inside Vim) | ![Latest GitHub commit](https://img.shields.io/github/last-commit/girishji/devdocs.vim?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/girishji/devdocs.vim?logo=github&label) |209| [girishji/devdocs.vim](https://github.com/girishji/devdocs.vim) | Vim plugin & TUI (browse inside Vim) | ![Latest GitHub commit](https://img.shields.io/github/last-commit/girishji/devdocs.vim?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/girishji/devdocs.vim?logo=github&label) |
209| [romainl/vim-devdocs](https://github.com/romainl/vim-devdocs) | Vim plugin | ![Latest GitHub commit](https://img.shields.io/github/last-commit/romainl/vim-devdocs?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/romainl/vim-devdocs?logo=github&label) |210| [romainl/vim-devdocs](https://github.com/romainl/vim-devdocs) | Vim plugin | ![Latest GitHub commit](https://img.shields.io/github/last-commit/romainl/vim-devdocs?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/romainl/vim-devdocs?logo=github&label) |
210| [waiting-for-dev/vim-www](https://github.com/waiting-for-dev/vim-www) | Vim plugin | ![Latest GitHub commit](https://img.shields.io/github/last-commit/waiting-for-dev/vim-www?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/waiting-for-dev/vim-www?logo=github&label) |211| [waiting-for-dev/vim-www](https://github.com/waiting-for-dev/vim-www) | Vim plugin | ![Latest GitHub commit](https://img.shields.io/github/last-commit/waiting-for-dev/vim-www?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/waiting-for-dev/vim-www?logo=github&label) |
211| [emmanueltouzery/apidocs.nvim](https://github.com/emmanueltouzery/apidocs.nvim) | Neovim plugin | ![Latest GitHub commit](https://img.shields.io/github/last-commit/emmanueltouzery/apidocs.nvim?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/emmanueltouzery/apidocs.nvim?logo=github&label) |212| [emmanueltouzery/apidocs.nvim](https://github.com/emmanueltouzery/apidocs.nvim) | Neovim plugin | ![Latest GitHub commit](https://img.shields.io/github/last-commit/emmanueltouzery/apidocs.nvim?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/emmanueltouzery/apidocs.nvim?logo=github&label) |
212| [toiletbril/dedoc](https://github.com/toiletbril/dedoc) | Terminal based viewer | ![Latest GitHub commit](https://img.shields.io/github/last-commit/toiletbril/dedoc?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/toiletbril/dedoc?logo=github&label) |213| [toiletbril/dedoc](https://github.com/toiletbril/dedoc) | Terminal based viewer | ![Latest GitHub commit](https://img.shields.io/github/last-commit/toiletbril/dedoc?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/toiletbril/dedoc?logo=github&label) |
213| [Raycast Devdocs](https://www.raycast.com/djpowers/devdocs) | Raycast extension | Unavailable | Unavailable |214| [Raycast Devdocs](https://www.raycast.com/djpowers/devdocs) | Raycast extension | Unavailable | Unavailable |
214| [chrisgrieser/alfred-docs-searches](https://github.com/chrisgrieser/alfred-docs-searches) | Alfred workflow | ![Latest GitHub commit](https://img.shields.io/github/last-commit/chrisgrieser/alfred-docs-searches?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/chrisgrieser/alfred-docs-searches?logo=github&label) |215| [chrisgrieser/alfred-docs-searches](https://github.com/chrisgrieser/alfred-docs-searches) | Alfred workflow | ![Latest GitHub commit](https://img.shields.io/github/last-commit/chrisgrieser/alfred-docs-searches?logo=github&label) | ![GitHub stars](https://img.shields.io/github/stars/chrisgrieser/alfred-docs-searches?logo=github&label) |
215 216
216## Copyright / License217## Copyright / License
lib/docs/filters/core/clean_text.rb+6−3
@@ -6,9 +6,12 @@ module Docs · 5 unmodified lines
6 6
7 def call7 def call
8 return html if context[:clean_text] == false8 return html if context[:clean_text] == false
9 html.strip!9
10 while html.gsub!(EMPTY_NODES_RGX, ''); end10 # Clone frozen literal.
11 html11 result = html.dup
12 result.strip!
13 while result.gsub!(EMPTY_NODES_RGX, ''); end
14 result
12 end15 end
13 end16 end
14end17end
lib/tasks/test.thor+7−0
@@ -26,4 +26,11 @@ class TestCLI < Thor · 25 unmodified lines
26 def app26 def app
27 require 'app_test'27 require 'app_test'
28 end28 end
29
30 desc 'coverage', 'Coverage report for core tests'
31 def coverage
32 ENV['COVERAGE'] = 'true'
33 require_relative '../../test/test_helper'
34 self.all
35 end
29end36end
test/lib/docs/core/doc_test.rb+393−0
@@ -1,5 +1,6 @@
1require_relative '../../../test_helper'1require_relative '../../../test_helper'
2require_relative '../../../../lib/docs'2require_relative '../../../../lib/docs'
3require 'base64'
3 4
4class DocsDocTest < Minitest::Spec5class DocsDocTest < Minitest::Spec
5 let :doc do6 let :doc do
@@ -237,6 +238,18 @@ class DocsDocTest < Minitest::Spec · 231 unmodified lines
237 doc.store_page(store, 'id')238 doc.store_page(store, 'id')
238 end239 end
239 end240 end
241
242 context "when the store can't be opened" do
243 before do
244 stub(store).open { raise Docs::SetupError, "oh no" }
245 end
246
247 it "logs an error" do
248 output = capture_io { doc.store_page(store, 'id') }.first
249
250 assert_match "ERROR: oh no", output
251 end
252 end
240 end253 end
241 254
242 describe ".store_pages" do255 describe ".store_pages" do
@@ -353,6 +366,18 @@ class DocsDocTest < Minitest::Spec · 110 unmodified lines
353 doc.store_pages(store)366 doc.store_pages(store)
354 end367 end
355 end368 end
369
370 context "when the store can't be opened" do
371 before do
372 stub(store).replace { raise Docs::SetupError, "oh no" }
373 end
374
375 it "logs an error" do
376 output = capture_io { doc.store_pages(store) }.first
377
378 assert_match "ERROR: oh no", output
379 end
380 end
356 end381 end
357 382
358 describe ".versions" do383 describe ".versions" do
@@ -406,4 +431,372 @@ class DocsDocTest < Minitest::Spec · 47 unmodified lines
406 assert_equal "Outdated minor version", instance.outdated_state('2006-01-02', '2006-02-03')431 assert_equal "Outdated minor version", instance.outdated_state('2006-01-02', '2006-02-03')
407 end432 end
408 end433 end
434
435 # PRIVATE CLASS METHODS
436
437 describe ".default_slug" do
438 it "returns name.downcase when name is alphanumeric" do
439 doc.name = 'FooBar'
440 assert_equal 'foobar', doc.send(:default_slug)
441 end
442
443 it "returns name.downcase when name contains underscores" do
444 doc.name = 'Foo_Bar'
445 assert_equal 'foo_bar', doc.send(:default_slug)
446 end
447
448 it "returns nil when name contains a hyphen" do
449 doc.name = 'Foo-Bar'
450 assert_nil doc.send(:default_slug)
451 end
452
453 it "returns nil when name contains a space" do
454 doc.name = 'Foo Bar'
455 assert_nil doc.send(:default_slug)
456 end
457
458 it "returns nil when name contains a dot" do
459 doc.name = 'Foo.Bar'
460 assert_nil doc.send(:default_slug)
461 end
462 end
463
464 describe ".store_page?" do
465 it "returns true when page has non-empty :entries" do
466 assert doc.send(:store_page?, page)
467 end
468
469 it "returns false when page has empty :entries" do
470 refute doc.send(:store_page?, page.merge(entries: []))
471 end
472
473 it "returns false when page has nil :entries" do
474 refute doc.send(:store_page?, page.merge(entries: nil))
475 end
476 end
477
478 describe ".store_index" do
479 let :index do
480 obj = Object.new
481 stub(obj).to_json { '{"entries":[]}' }
482 obj
483 end
484
485 context "when read_write is true (default)" do
486 it "reads old JSON from store" do
487 mock(store).read('index.json') { '{}' }
488 stub(store).write
489 doc.send(:store_index, store, 'index.json', index)
490 end
491
492 it "writes new JSON to store" do
493 stub(store).read { '{}' }
494 mock(store).write('index.json', '{"entries":[]}')
495 doc.send(:store_index, store, 'index.json', index)
496 end
497 end
498
499 context "when read_write is false" do
500 it "doesn't read from store" do
501 dont_allow(store).read
502 doc.send(:store_index, store, 'index.json', index, false)
503 end
504
505 it "doesn't write to store" do
506 dont_allow(store).write
507 doc.send(:store_index, store, 'index.json', index, false)
508 end
509 end
510 end
511
512 describe ".store_meta" do
513 it "writes to META_FILENAME" do
514 stub(store).size { 0 }
515 mock(store).write(Docs::Doc::META_FILENAME, anything)
516 doc.send(:store_meta, store)
517 end
518
519 it "includes :mtime as an integer in the written JSON" do
520 stub(store).size { 0 }
521 stub(store).write(Docs::Doc::META_FILENAME, anything) do |_, json|
522 @json = JSON.parse(json)
523 end
524 doc.send(:store_meta, store)
525 assert @json.key?('mtime')
526 assert_instance_of Integer, @json['mtime']
527 end
528
529 it "includes :db_size from the store" do
530 stub(store).size(Docs::Doc::DB_FILENAME) { 42 }
531 stub(store).write(Docs::Doc::META_FILENAME, anything) do |_, json|
532 @json = JSON.parse(json)
533 end
534 doc.send(:store_meta, store)
535 assert_equal 42, @json['db_size']
536 end
537 end
538
539 # PRIVATE INSTANCE METHODS
540
541 describe "#fetch" do
542 let :instance do
543 doc.new
544 end
545
546 let :logger do
547 l = Object.new
548 stub(l).debug
549 stub(l).error
550 l
551 end
552
553 let :opts do
554 { logger: logger }
555 end
556
557 let :response do
558 r = Object.new
559 stub(r).success? { true }
560 stub(r).body { 'body' }
561 r
562 end
563
564 before do
565 stub(Docs::Request).run { response }
566 end
567
568 it "returns the response body on success" do
569 assert_equal 'body', instance.send(:fetch, 'http://example.com/', opts)
570 end
571
572 it "calls Request.run with the given url" do
573 mock(Docs::Request).run('http://example.com/', anything) { response }
574 instance.send(:fetch, 'http://example.com/', opts)
575 end
576
577 it "adds Authorization header when opts has :github_token and url is GitHub API" do
578 opts[:github_token] = 'mytoken'
579 mock(Docs::Request).run(anything, anything) do |_, request_opts|
580 assert_equal 'token mytoken', request_opts[:headers]['Authorization']
581 response
582 end
583 instance.send(:fetch, 'https://api.github.com/repos', opts)
584 end
585
586 it "doesn't add Authorization header for non-GitHub API urls even with :github_token" do
587 opts[:github_token] = 'mytoken'
588 mock(Docs::Request).run(anything, anything) do |_, request_opts|
589 refute request_opts[:headers].key?('Authorization')
590 response
591 end
592 instance.send(:fetch, 'http://example.com/', opts)
593 end
594
595 context "when response is unsuccessful" do
596 before do
597 stub(response).success? { false }
598 stub(response).timed_out? { false }
599 stub(response).code { 404 }
600 end
601
602 it "raises an error" do
603 assert_raises(RuntimeError) do
604 instance.send(:fetch, 'http://example.com/', opts)
605 end
606 end
607
608 it "includes the response code in the error message" do
609 err = assert_raises(RuntimeError) { instance.send(:fetch, 'http://example.com/', opts) }
610 assert_match '404', err.message
611 end
612 end
613
614 context "when response times out" do
615 before do
616 stub(response).success? { false }
617 stub(response).timed_out? { true }
618 end
619
620 it "raises an error with a timeout message" do
621 err = assert_raises(RuntimeError) { instance.send(:fetch, 'http://example.com/', opts) }
622 assert_match 'Timed out', err.message
623 end
624 end
625 end
626
627 describe "#fetch_doc" do
628 let :instance do
629 doc.new
630 end
631
632 let :opts do
633 { logger: nil }
634 end
635
636 it "returns a Nokogiri::HTML::Document" do
637 stub(instance).fetch { '<html><body></body></html>' }
638 assert_instance_of Nokogiri::HTML::Document, instance.send(:fetch_doc, 'http://example.com/', opts)
639 end
640
641 it "fetches the given url" do
642 mock(instance).fetch('http://example.com/', opts) { '<html></html>' }
643 instance.send(:fetch_doc, 'http://example.com/', opts)
644 end
645 end
646
647 describe "#fetch_json" do
648 let :instance do
649 doc.new
650 end
651
652 let :opts do
653 { logger: nil }
654 end
655
656 it "returns parsed JSON" do
657 stub(instance).fetch { '{"key":"value"}' }
658 assert_equal({ 'key' => 'value' }, instance.send(:fetch_json, 'http://example.com/', opts))
659 end
660
661 it "fetches the given url" do
662 mock(instance).fetch('http://example.com/', opts) { '{}' }
663 instance.send(:fetch_json, 'http://example.com/', opts)
664 end
665 end
666
667 describe "#get_npm_version" do
668 let :instance do
669 doc.new
670 end
671
672 let :opts do
673 { logger: nil }
674 end
675
676 it "returns the 'latest' dist-tag by default" do
677 stub(instance).fetch_json { { 'dist-tags' => { 'latest' => '1.2.3' } } }
678 assert_equal '1.2.3', instance.send(:get_npm_version, 'some-package', opts)
679 end
680
681 it "returns a specific dist-tag when given" do
682 stub(instance).fetch_json { { 'dist-tags' => { 'next' => '2.0.0-beta' } } }
683 assert_equal '2.0.0-beta', instance.send(:get_npm_version, 'some-package', opts, 'next')
684 end
685
686 it "fetches from the npm registry URL" do
687 mock(instance).fetch_json('https://registry.npmjs.com/some-package', opts) { { 'dist-tags' => { 'latest' => '1.0.0' } } }
688 instance.send(:get_npm_version, 'some-package', opts)
689 end
690 end
691
692 describe "#get_latest_github_release" do
693 let :instance do
694 doc.new
695 end
696
697 let :opts do
698 { logger: nil }
699 end
700
701 it "strips a leading 'v' from the tag name" do
702 stub(instance).fetch_json { { 'tag_name' => 'v1.2.3' } }
703 assert_equal '1.2.3', instance.send(:get_latest_github_release, 'owner', 'repo', opts)
704 end
705
706 it "returns the tag name unchanged when it doesn't start with 'v'" do
707 stub(instance).fetch_json { { 'tag_name' => '1.2.3' } }
708 assert_equal '1.2.3', instance.send(:get_latest_github_release, 'owner', 'repo', opts)
709 end
710
711 it "fetches from the correct GitHub releases URL" do
712 mock(instance).fetch_json('https://api.github.com/repos/owner/repo/releases/latest', opts) { { 'tag_name' => '1.0.0' } }
713 instance.send(:get_latest_github_release, 'owner', 'repo', opts)
714 end
715 end
716
717 describe "#get_github_tags" do
718 let :instance do
719 doc.new
720 end
721
722 let :opts do
723 { logger: nil }
724 end
725
726 it "fetches from the correct GitHub tags URL" do
727 mock(instance).fetch_json('https://api.github.com/repos/owner/repo/tags', opts) { [] }
728 instance.send(:get_github_tags, 'owner', 'repo', opts)
729 end
730
731 it "returns the tags list" do
732 tags = [{ 'name' => 'v1.0' }]
733 stub(instance).fetch_json { tags }
734 assert_equal tags, instance.send(:get_github_tags, 'owner', 'repo', opts)
735 end
736 end
737
738 describe "#get_github_file_contents" do
739 let :instance do
740 doc.new
741 end
742
743 let :opts do
744 { logger: nil }
745 end
746
747 it "returns Base64-decoded file contents" do
748 stub(instance).fetch_json { { 'content' => ::Base64.encode64('hello world') } }
749 assert_equal 'hello world', instance.send(:get_github_file_contents, 'owner', 'repo', 'README.md', opts)
750 end
751
752 it "fetches from the correct GitHub contents URL" do
753 mock(instance).fetch_json('https://api.github.com/repos/owner/repo/contents/README.md', opts) { { 'content' => ::Base64.encode64('') } }
754 instance.send(:get_github_file_contents, 'owner', 'repo', 'README.md', opts)
755 end
756 end
757
758 describe "#get_latest_github_commit_date" do
759 let :instance do
760 doc.new
761 end
762
763 let :opts do
764 { logger: nil }
765 end
766
767 it "returns the epoch timestamp of the latest commit" do
768 date_str = '2023-06-15T12:00:00Z'
769 commits = [{ 'commit' => { 'author' => { 'date' => date_str } } }]
770 stub(instance).fetch_json { commits }
771 assert_equal Date.iso8601(date_str).to_time.to_i, instance.send(:get_latest_github_commit_date, 'owner', 'repo', opts)
772 end
773
774 it "fetches from the correct GitHub commits URL" do
775 date_str = '2023-06-15T12:00:00Z'
776 commits = [{ 'commit' => { 'author' => { 'date' => date_str } } }]
777 mock(instance).fetch_json('https://api.github.com/repos/owner/repo/commits', opts) { commits }
778 instance.send(:get_latest_github_commit_date, 'owner', 'repo', opts)
779 end
780 end
781
782 describe "#get_gitlab_tags" do
783 let :instance do
784 doc.new
785 end
786
787 let :opts do
788 { logger: nil }
789 end
790
791 it "fetches from the correct GitLab tags URL" do
792 mock(instance).fetch_json('https://gitlab.example.com/api/v4/projects/group%2Fproject/repository/tags', opts) { [] }
793 instance.send(:get_gitlab_tags, 'gitlab.example.com', 'group', 'project', opts)
794 end
795
796 it "returns the tags list" do
797 tags = [{ 'name' => 'v1.0' }]
798 stub(instance).fetch_json { tags }
799 assert_equal tags, instance.send(:get_gitlab_tags, 'gitlab.example.com', 'group', 'project', opts)
800 end
801 end
409end802end
test/lib/docs/core/entry_index_test.rb+66−0
@@ -154,6 +154,72 @@ class DocsEntryIndexTest < Minitest::Spec · 153 unmodified lines
154 end154 end
155 end155 end
156 156
157 describe "#sort_fn" do
158 def sort(a, b)
159 index.send(:sort_fn, a, b)
160 end
161
162 context "when neither string starts with a digit 1-9" do
163 it "uses case-insensitive string comparison" do
164 assert_operator sort('apple', 'banana'), :<, 0
165 assert_operator sort('banana', 'apple'), :>, 0
166 end
167
168 it "returns 0 for case-insensitive equal strings" do
169 assert_equal 0, sort('Apple', 'apple')
170 end
171
172 it "treats strings starting with '0' as non-numeric" do
173 assert_equal '0apple'.casecmp('banana'), sort('0apple', 'banana')
174 end
175 end
176
177 context "when at least one string starts with 1-9 but neither contains version dots" do
178 it "uses case-insensitive string comparison" do
179 # lexicographic: '1' < '2', so '10 Test' < '2 Test'
180 assert_operator sort('10 Test', '2 Test'), :<, 0
181 end
182
183 it "returns 0 for case-insensitive equal strings" do
184 assert_equal 0, sort('2 Test', '2 test')
185 end
186 end
187
188 context "when one string has version dots and the other does not" do
189 it "sorts the dotted string before the undotted one" do
190 assert_operator sort('1.2. Test', '2 Test'), :<, 0
191 end
192
193 it "sorts the undotted string after the dotted one" do
194 assert_operator sort('2 Test', '1.2. Test'), :>, 0
195 end
196
197 it "sorts the dotted string before a non-numeric string" do
198 assert_operator sort('1.2. Test', 'abc'), :<, 0
199 end
200 end
201
202 context "when both strings have version dots" do
203 it "sorts numerically rather than lexicographically" do
204 assert_operator sort('1.9. Test', '1.10. Test'), :<, 0
205 assert_operator sort('4.20. Test', '4.3. Test'), :>, 0
206 end
207
208 it "pads shorter version depths with zeros before comparing" do
209 assert_operator sort('4. Test', '4.2. Test'), :<, 0
210 assert_operator sort('4.2. Test', '4. Test'), :>, 0
211 end
212
213 it "compares multi-level versions against shorter ones correctly" do
214 assert_operator sort('4.2.2. Test', '4.3. Test'), :<, 0
215 end
216
217 it "returns 0 for identical version strings" do
218 assert_equal 0, sort('1.2. Test', '1.2. Test')
219 end
220 end
221 end
222
157 describe "#to_json" do223 describe "#to_json" do
158 it "returns the JSON string for #as_json" do224 it "returns the JSON string for #as_json" do
159 stub(index).as_json { { entries: [1], types: [2] } }225 stub(index).as_json { { entries: [1], types: [2] } }
test/lib/docs/core/filter_test.rb+38−0
@@ -8,6 +8,12 @@ class DocsFilterTest < Minitest::Spec · 7 unmodified lines
8 before do8 before do
9 context[:base_url] = 'http://example.com/path'9 context[:base_url] = 'http://example.com/path'
10 context[:url] = 'http://example.com/path/file'10 context[:url] = 'http://example.com/path/file'
11 context[:version] = '6.1'
12 context[:release] = '6.1.1'
13 context[:links] = {
14 home: 'homunculus',
15 code: 'codified'
16 }
11 end17 end
12 18
13 describe "#subpath" do19 describe "#subpath" do
@@ -55,6 +61,24 @@ class DocsFilterTest < Minitest::Spec · 41 unmodified lines
55 end61 end
56 end62 end
57 63
64 describe "#version" do
65 it "returns the version" do
66 assert_equal filter.version, "6.1"
67 end
68 end
69
70 describe "#release" do
71 it "returns the release" do
72 assert_equal filter.release, "6.1.1"
73 end
74 end
75
76 describe "#links" do
77 it "returns the links hash" do
78 assert_equal filter.links, { home: 'homunculus', code: 'codified' }
79 end
80 end
81
58 describe "#root_page?" do82 describe "#root_page?" do
59 it "returns true when #subpath is blank" do83 it "returns true when #subpath is blank" do
60 stub(filter).subpath { '' }84 stub(filter).subpath { '' }
@@ -176,4 +200,18 @@ class DocsFilterTest < Minitest::Spec · 115 unmodified lines
176 refute filter.absolute_url_string?('http')200 refute filter.absolute_url_string?('http')
177 end201 end
178 end202 end
203
204 describe "#clean_path" do
205 it "replaces some special characters with dashes" do
206 bad_path = "/horrible!url:design;huh".freeze
207 good_path = "/horrible-url-design-huh".freeze
208 assert_equal filter.clean_path(bad_path), good_path
209 end
210
211 it "replaces + with the word 'plus'" do
212 bad_path = "/c++".freeze
213 good_path = "/c_plus__plus_".freeze
214 assert_equal filter.clean_path(bad_path), good_path
215 end
216 end
179end217end
test/lib/docs/core/scraper_test.rb+162−0
@@ -510,4 +510,166 @@ class DocsScraperTest < Minitest::Spec · 509 unmodified lines
510 assert_equal Docs, pipeline.instrumentation_service510 assert_equal Docs, pipeline.instrumentation_service
511 end511 end
512 end512 end
513
514 # PRIVATE INSTANCE METHODS
515
516 describe "#url_for" do
517 it "returns the root url when path is empty" do
518 assert_equal scraper.root_url.to_s, scraper.send(:url_for, '')
519 end
520
521 it "returns the root url when path is '/'" do
522 assert_equal scraper.root_url.to_s, scraper.send(:url_for, '/')
523 end
524
525 it "joins base_url and path for a non-root path" do
526 stub(Scraper).base_url { 'http://example.com/' }
527 assert_equal 'http://example.com/page', scraper.send(:url_for, '/page')
528 end
529 end
530
531 describe "#parse" do
532 it "returns a two-element array" do
533 response.body = '<html><head><title>T</title></head><body></body></html>'
534 assert_equal 2, scraper.send(:parse, response).length
535 end
536
537 it "extracts the title from an HTML document" do
538 response.body = '<html><head><title>My Title</title></head><body></body></html>'
539 _, title = scraper.send(:parse, response)
540 assert_equal 'My Title', title
541 end
542
543 it "returns nil title for an HTML fragment" do
544 response.body = '<div>just a fragment</div>'
545 _, title = scraper.send(:parse, response)
546 assert_nil title
547 end
548 end
549
550 describe "#pipeline_context" do
551 it "returns options merged with the response url" do
552 stub(scraper).options { { base_url: 'http://example.com/' } }
553 response.url = 'http://example.com/page'
554 context = scraper.send(:pipeline_context, response)
555 assert_equal 'http://example.com/page', context[:url]
556 assert_equal 'http://example.com/', context[:base_url]
557 end
558
559 it "does not mutate the options hash" do
560 opts = { base_url: 'http://example.com/' }
561 stub(scraper).options { opts }
562 response.url = 'url'
563 scraper.send(:pipeline_context, response)
564 refute opts.key?(:url)
565 end
566 end
567
568 describe "#process_response" do
569 before do
570 stub(scraper).parse(response) { ['<div></div>', 'My Title'] }
571 response.url = 'http://example.com/'
572 end
573
574 it "returns a hash" do
575 assert_instance_of Hash, scraper.send(:process_response, response)
576 end
577
578 it "passes :html_title from parse into the pipeline context" do
579 mock(scraper.pipeline).call(anything, anything, anything) do |_, context, _|
580 assert_equal 'My Title', context[:html_title]
581 end
582 scraper.send(:process_response, response)
583 end
584 end
585
586 describe "#with_filters" do
587 it "uses only the given filters inside the block" do
588 scraper.send(:with_filters) do
589 @filters = scraper.pipeline.filters
590 end
591 assert_equal [], @filters
592 end
593
594 it "resets the pipeline after the block" do
595 pipeline_before = scraper.pipeline
596 scraper.send(:with_filters) {}
597 refute_same pipeline_before, scraper.pipeline
598 end
599 end
600
601 describe "#additional_options" do
602 it "returns an empty hash" do
603 assert_equal({}, scraper.send(:additional_options))
604 end
605 end
606
607 class FixedScraper < Scraper
608 include Docs::Scraper::FixInternalUrlsBehavior
609 end
610
611 describe "FixInternalUrlsBehavior" do
612 let :fixed_scraper do
613 FixedScraper.new.tap { |s| s.extend FakeInstrumentation }
614 end
615
616 describe ".with_internal_urls (private)" do
617 it "sets .internal_urls to the result of fetch_internal_urls during the block" do
618 any_instance_of(FixedScraper) do |instance|
619 stub(instance).fetch_internal_urls { ['url1', 'url2'] }
620 end
621 FixedScraper.send(:with_internal_urls) do
622 @urls = FixedScraper.internal_urls
623 end
624 assert_equal ['url1', 'url2'], @urls
625 end
626
627 it "clears .internal_urls after the block" do
628 any_instance_of(FixedScraper) do |instance|
629 stub(instance).fetch_internal_urls { [] }
630 end
631 FixedScraper.send(:with_internal_urls) {}
632 assert_nil FixedScraper.internal_urls
633 end
634 end
635
636 describe "#additional_options (private)" do
637 it "returns an empty hash when .internal_urls is nil" do
638 stub(FixedScraper).internal_urls { nil }
639 assert_equal({}, fixed_scraper.send(:additional_options))
640 end
641
642 it "returns fixed_internal_urls: true when .internal_urls is set" do
643 stub(FixedScraper).internal_urls { ['url'] }
644 assert fixed_scraper.send(:additional_options)[:fixed_internal_urls]
645 end
646
647 it "sets :only to a Set of .internal_urls" do
648 stub(FixedScraper).internal_urls { ['url1', 'url2'] }
649 assert_equal Set.new(['url1', 'url2']), fixed_scraper.send(:additional_options)[:only]
650 end
651
652 it "sets :only_patterns to nil" do
653 stub(FixedScraper).internal_urls { ['url'] }
654 assert_nil fixed_scraper.send(:additional_options)[:only_patterns]
655 end
656
657 it "sets :skip to nil" do
658 stub(FixedScraper).internal_urls { ['url'] }
659 assert_nil fixed_scraper.send(:additional_options)[:skip]
660 end
661 end
662
663 describe "#process_response (private)" do
664 before do
665 stub(fixed_scraper).parse(response) { ['<div></div>', nil] }
666 response.url = 'http://example.com/page'
667 end
668
669 it "merges :response_url from the response into the result" do
670 result = fixed_scraper.send(:process_response, response)
671 assert_equal 'http://example.com/page', result[:response_url]
672 end
673 end
674 end
513end675end
test/lib/docs/core/scrapers/url_scraper_test.rb+62−0
@@ -85,6 +85,38 @@ class DocsUrlScraperTest < Minitest::Spec · 84 unmodified lines
85 stub(Docs::Requester).run { 'response' }85 stub(Docs::Requester).run { 'response' }
86 assert_equal 'response', result86 assert_equal 'response', result
87 end87 end
88
89 context "when options[:rate_limit] is set" do
90 before do
91 @saved_limiter = Docs::UrlScraper.class_variable_get(:@@rate_limiter)
92 Docs::UrlScraper.class_variable_set(:@@rate_limiter, nil)
93 stub(Scraper).options { { rate_limit: 5 } }
94 stub(Docs::Requester).run
95 end
96
97 after do
98 Docs::UrlScraper.class_variable_set(:@@rate_limiter, @saved_limiter)
99 end
100
101 it "creates a RateLimiter with the given limit" do
102 stub(Typhoeus).before
103 scraper.send :request_all, 'urls'
104 assert_equal 5, Docs::UrlScraper.class_variable_get(:@@rate_limiter).limit
105 end
106
107 it "registers the RateLimiter with Typhoeus.before" do
108 mock(Typhoeus).before
109 scraper.send :request_all, 'urls'
110 end
111
112 it "updates the existing RateLimiter's limit instead of creating a new one" do
113 existing = Docs::UrlScraper::RateLimiter.new(3)
114 Docs::UrlScraper.class_variable_set(:@@rate_limiter, existing)
115 dont_allow(Typhoeus).before
116 scraper.send :request_all, 'urls'
117 assert_equal 5, existing.limit
118 end
119 end
88 end120 end
89 121
90 describe "#process_response?" do122 describe "#process_response?" do
@@ -120,4 +152,34 @@ class DocsUrlScraperTest < Minitest::Spec · 29 unmodified lines
120 assert result152 assert result
121 end153 end
122 end154 end
155
156 describe "RateLimiter" do
157 let :limiter do
158 Docs::UrlScraper::RateLimiter.new(3)
159 end
160
161 describe "#initialize" do
162 it "sets the limit" do
163 assert_equal 3, limiter.limit
164 end
165 end
166
167 describe "#limit=" do
168 it "updates the limit" do
169 limiter.limit = 10
170 assert_equal 10, limiter.limit
171 end
172 end
173
174 describe "#to_proc" do
175 it "returns a Proc" do
176 assert_instance_of Proc, limiter.to_proc
177 end
178
179 it "returns a proc that calls #call" do
180 mock(limiter).call
181 limiter.to_proc.call
182 end
183 end
184 end
123end185end
test/lib/docs/core/subscriber_test.rb+98−0
@@ -0,0 +1,98 @@
1# frozen_string_literal: true
2
3require_relative '../../../test_helper'
4require_relative '../../../../lib/docs'
5
6class DocsSubscriberTest < Minitest::Spec
7 let(:subscriber) { Docs::Subscriber.new }
8
9 describe "#format_url" do
10 it "removes http:// scheme" do
11 assert_equal 'devdocs.test.co/path', subscriber.send(:format_url, 'http://devdocs.test.co/path')
12 end
13
14 it "removes https:// scheme" do
15 assert_equal 'devdocs.test.co/path', subscriber.send(:format_url, 'https://devdocs.test.co/path')
16 end
17
18 it "returns non-http urls unchanged" do
19 assert_equal 'devdocs.test.co/path', subscriber.send(:format_url, 'devdocs.test.co/path')
20 end
21 end
22
23 describe "#format_path" do
24 it "removes the current working directory prefix" do
25 full_path = File.join(Dir.pwd, 'some/file.rb')
26 assert_equal 'some/file.rb', subscriber.send(:format_path, full_path)
27 end
28
29 it "returns paths outside the cwd unchanged" do
30 assert_equal '/other/path', subscriber.send(:format_path, '/other/path')
31 end
32
33 it "calls to_s on the argument" do
34 path = Object.new
35 def path.to_s; '/other/path'; end
36 assert_equal '/other/path', subscriber.send(:format_path, path)
37 end
38 end
39
40 describe "#justify" do
41 describe "when terminal_width is nil" do
42 before { subscriber.instance_variable_set(:@terminal_width, nil) }
43
44 it "returns the string unchanged" do
45 assert_equal 'hello', subscriber.send(:justify, 'hello')
46 end
47 end
48
49 describe "when terminal_width is set" do
50 before { subscriber.instance_variable_set(:@terminal_width, 20) }
51
52 it "pads a short string to terminal width" do
53 result = subscriber.send(:justify, 'hi')
54 assert_equal 20, result.length
55 assert result.start_with?('hi')
56 end
57
58 it "truncates a long string to terminal width" do
59 result = subscriber.send(:justify, 'a' * 30)
60 assert_equal 20, result.length
61 end
62
63 it "reserves space for a trailing tag and pads the rest" do
64 result = subscriber.send(:justify, 'hi [tag]')
65 assert_equal 20, result.length
66 assert result.end_with?('[tag]')
67 end
68
69 it "truncates the non-tag part when the string is too long" do
70 result = subscriber.send(:justify, ('a' * 30) + ' [tag]')
71 assert_equal 20, result.length
72 assert result.end_with?('[tag]')
73 end
74 end
75 end
76
77 describe "#terminal_width" do
78 it "returns nil when stdout is not a tty" do
79 old_stdout = $stdout
80 $stdout = StringIO.new
81 sub = Docs::Subscriber.new
82 assert_nil sub.send(:terminal_width)
83 ensure
84 $stdout = old_stdout
85 end
86
87 it "reads COLUMNS env var when stdout is a tty" do
88 sub = Docs::Subscriber.new
89 # Override tty? on the instance to avoid needing a real tty
90 def sub.tty?; true; end
91 old_columns = ENV.delete('COLUMNS')
92 ENV['COLUMNS'] = '666'
93 assert_equal 666, sub.send(:terminal_width)
94 ensure
95 ENV['COLUMNS'] = old_columns
96 end
97 end
98end
test/lib/docs/filters/core/attribution_test.rb+39−0
@@ -0,0 +1,39 @@
1require_relative '../../../../test_helper'
2require_relative '../../../../../lib/docs'
3
4class AttributionFilterTest < Minitest::Spec
5 include FilterTestHelper
6 self.filter_class = Docs::AttributionFilter
7 self.filter_type = 'html'
8
9 before do
10 @body = "<div class=\"doc\">Hello!</div>"
11 end
12
13 context "when attribution is a string" do
14 before do
15 context[:attribution] = "<span id=\"mtwain\">Copyright 2033 Mark Twain</span>"
16 context[:base_url] = "q"
17 end
18
19 it "adds it to the document" do
20 assert_equal filter_output.at_css('#mtwain').inner_html, "Copyright 2033 Mark Twain"
21 refute_nil filter_output.at_css('p._attribution-p'), "Attribution node should exist"
22 refute_nil filter_output.at_css('.doc'), "Existing doc should still be present"
23 refute_nil filter_output.at_css('a._attribution-link'), "Attribution link should exist"
24 end
25 end
26
27 context "when attribution is a Proc" do
28 before do
29 context[:attribution] = ->(filter) {
30 "<span id=\"mtwain\">Copyright 2034 Mark Twain</span>"
31 }
32 context[:base_url] = "q"
33 end
34
35 it "adds its output to the document" do
36 assert_equal filter_output.at_css('#mtwain').inner_html, "Copyright 2034 Mark Twain"
37 end
38 end
39end
test/lib/docs/filters/core/clean_local_urls_test.rb+22−0
@@ -0,0 +1,22 @@
1require_relative '../../../../test_helper'
2require_relative '../../../../../lib/docs'
3
4class CleanLocalUrlsFilterTest < Minitest::Spec
5 include FilterTestHelper
6 self.filter_class = Docs::CleanLocalUrlsFilter
7
8 it "removes images/iframes referencing localhost" do
9 context[:base_url] = 'http://localhost'
10 @body = '<img src="http://localhost/img.png"><iframe src="http://localhost/frame"></iframe>'
11 assert_empty filter_output.css('img, iframe')
12 end
13
14 it "turns links referencing localhost into plain text" do
15 context[:base_url] = 'http://localhost'
16 @body = '<a href="http://localhost/page">Link</a>'
17 node = filter_output.at_css('span')
18 assert node, 'expected a <span> node'
19 refute node['href']
20 assert_equal 'Link', node.text
21 end
22end
test/lib/docs/filters/core/images_test.rb+133−0
@@ -0,0 +1,133 @@
1require_relative '../../../../test_helper'
2require_relative '../../../../../lib/docs'
3
4class ImagesFilterTest < Minitest::Spec
5 include FilterTestHelper
6 self.filter_class = Docs::ImagesFilter
7
8 IMG_SRC = 'http://example.com/img.png'
9 IMG_BODY = '<img src="http://example.com/img.png">'
10
11 before do
12 Docs::ImagesFilter.cache.clear
13 context[:optimize_images] = false
14 end
15
16 def make_response(success: true, mime_type: 'image/png', content_length: 100, body: 'imgdata', code: 200)
17 OpenStruct.new(success?: success, mime_type: mime_type, content_length: content_length, body: body, code: code)
18 end
19
20 def stub_request(response)
21 stub(Docs::Request).run { |_url, &block| block.call(response) }
22 end
23
24 context "when download_images is false" do
25 it "returns doc unchanged and makes no requests" do
26 context[:download_images] = false
27 @body = IMG_BODY
28 dont_allow(Docs::Request).run
29 assert_equal IMG_SRC, filter_output.at_css('img')['src']
30 end
31 end
32
33 context "with a data: URL" do
34 it "skips the image and makes no request" do
35 @body = '<img src="data:image/png;base64,aaaaaa2320345bbb234">'
36 dont_allow(Docs::Request).run
37 assert_equal 'data:image/png;base64,aaaaaa2320345bbb234', filter_output.at_css('img')['src']
38 end
39 end
40
41 context "with a non-http(s) URL" do
42 it "skips the image and makes no request" do
43 @body = '<img src="ftp://example.com/img.png">'
44 dont_allow(Docs::Request).run
45 filter_output
46 end
47 end
48
49 context "with a successful image response" do
50 it "replaces src with a base64 data URI" do
51 @body = IMG_BODY
52 stub_request make_response(body: 'imgdata', mime_type: 'image/png', content_length: 7)
53 expected = "data:image/png;base64,#{Base64.strict_encode64('imgdata')}"
54 assert_equal expected, filter_output.at_css('img')['src']
55 end
56
57 it "caches the data URI for subsequent calls" do
58 @body = IMG_BODY
59 stub_request make_response(body: 'imgdata', mime_type: 'image/png', content_length: 7)
60 filter_output
61 assert Docs::ImagesFilter.cache.key?(IMG_SRC)
62 assert Docs::ImagesFilter.cache[IMG_SRC].start_with?('data:')
63 end
64 end
65
66 context "when response is not successful" do
67 it "instruments broken.image" do
68 @body = IMG_BODY
69 stub_request make_response(success: false, code: 404)
70 @called = false
71 filter.subscribe('broken.image') { @called = true }
72 filter_output
73 assert @called
74 end
75
76 it "does not alter the src" do
77 @body = IMG_BODY
78 stub_request make_response(success: false, code: 404)
79 assert_equal IMG_SRC, filter_output.at_css('img')['src']
80 end
81 end
82
83 context "when response mime type is not image/" do
84 it "instruments invalid.image" do
85 @body = IMG_BODY
86 stub_request make_response(mime_type: 'text/html')
87 @called = false
88 filter.subscribe('invalid.image') { @called = true }
89 filter_output
90 assert @called
91 end
92 end
93
94 context "when an exception is raised during the request" do
95 it "instruments error.image" do
96 @body = IMG_BODY
97 stub(Docs::Request).run { raise 'connection error' }
98 @called = false
99 filter.subscribe('error.image') { @called = true }
100 filter_output
101 assert @called
102 end
103 end
104
105 context "with a cached src" do
106 it "uses the cached data URI without making a request" do
107 cached = "data:image/png;base64,#{Base64.strict_encode64('cached')}"
108 Docs::ImagesFilter.cache[IMG_SRC] = cached
109 @body = IMG_BODY
110 dont_allow(Docs::Request).run
111 assert_equal cached, filter_output.at_css('img')['src']
112 end
113
114 it "leaves src unchanged when cache marks the URL as failed" do
115 Docs::ImagesFilter.cache[IMG_SRC] = false
116 @body = IMG_BODY
117 dont_allow(Docs::Request).run
118 assert_equal IMG_SRC, filter_output.at_css('img')['src']
119 end
120 end
121
122 context "when optimize_images is not disabled" do
123 it "passes image data through optimize_image_data" do
124 @body = IMG_BODY
125 optimized = 'optimized_imgdata'
126 stub_request make_response(body: 'imgdata', mime_type: 'image/png', content_length: 7)
127 stub(Docs::ImagesFilter).optimize_image_data('imgdata') { optimized }
128 context.delete(:optimize_images)
129 expected = "data:image/png;base64,#{Base64.strict_encode64(optimized)}"
130 assert_equal expected, filter_output.at_css('img')['src']
131 end
132 end
133end
test/test_helper.rb+10−0
@@ -1,3 +1,13 @@
1# Flag set by Thor coverage task.
2if ENV['COVERAGE'] == 'true'
3 require 'simplecov'
4
5 # Focus only on core app files.
6 SimpleCov.add_filter {|src| !(src.filename =~ /\/core\//)}
7
8 SimpleCov.start 'rails'
9end
10
1ENV['RACK_ENV'] = 'test'11ENV['RACK_ENV'] = 'test'
2 12
3require 'bundler/setup'13require 'bundler/setup'