|
| 1 | +# frozen_string_literal: true |
| 2 | +require_relative '../helper' |
| 3 | + |
| 4 | +class RDocGeneratorAlikiTest < RDoc::TestCase |
| 5 | + |
| 6 | + def setup |
| 7 | + super |
| 8 | + |
| 9 | + @lib_dir = "#{@pwd}/lib" |
| 10 | + $LOAD_PATH.unshift @lib_dir |
| 11 | + |
| 12 | + @options = RDoc::Options.new |
| 13 | + @options.option_parser = OptionParser.new |
| 14 | + |
| 15 | + @tmpdir = File.join Dir.tmpdir, "test_rdoc_generator_aliki_#{$$}" |
| 16 | + FileUtils.mkdir_p @tmpdir |
| 17 | + Dir.chdir @tmpdir |
| 18 | + @options.op_dir = @tmpdir |
| 19 | + @options.generator = RDoc::Generator::Aliki |
| 20 | + |
| 21 | + $LOAD_PATH.each do |path| |
| 22 | + aliki_dir = File.join path, 'rdoc/generator/template/aliki/' |
| 23 | + next unless File.directory? aliki_dir |
| 24 | + @options.template_dir = aliki_dir |
| 25 | + break |
| 26 | + end |
| 27 | + |
| 28 | + @rdoc.options = @options |
| 29 | + |
| 30 | + @g = @options.generator.new @store, @options |
| 31 | + @rdoc.generator = @g |
| 32 | + |
| 33 | + @top_level = @store.add_file 'file.rb' |
| 34 | + @top_level.parser = RDoc::Parser::Ruby |
| 35 | + @klass = @top_level.add_class RDoc::NormalClass, 'Klass' |
| 36 | + |
| 37 | + @meth = RDoc::AnyMethod.new nil, 'method' |
| 38 | + @meth_with_html_tag_yield = RDoc::AnyMethod.new nil, 'method_with_html_tag_yield' |
| 39 | + @meth_with_html_tag_yield.block_params = '%<<script>alert("atui")</script>>, yield_arg' |
| 40 | + |
| 41 | + @klass.add_method @meth |
| 42 | + @klass.add_method @meth_with_html_tag_yield |
| 43 | + |
| 44 | + @store.complete :private |
| 45 | + end |
| 46 | + |
| 47 | + def teardown |
| 48 | + super |
| 49 | + |
| 50 | + $LOAD_PATH.shift |
| 51 | + Dir.chdir @pwd |
| 52 | + FileUtils.rm_rf @tmpdir |
| 53 | + end |
| 54 | + |
| 55 | + def test_inheritance_and_template_dir |
| 56 | + assert_kind_of RDoc::Generator::Darkfish, @g |
| 57 | + assert_match %r{/template/aliki\z}, @g.template_dir.to_s |
| 58 | + end |
| 59 | + |
| 60 | + def test_write_style_sheet_copies_css_and_js_only |
| 61 | + @g.generate |
| 62 | + |
| 63 | + # Aliki should have these assets |
| 64 | + assert_file 'css/rdoc.css' |
| 65 | + assert_file 'js/aliki.js' |
| 66 | + assert_file 'js/search.js' |
| 67 | + assert_file 'js/theme-toggle.js' |
| 68 | + assert_file 'js/c_highlighter.js' |
| 69 | + |
| 70 | + # Aliki should NOT have fonts (unlike Darkfish) |
| 71 | + refute File.exist?('css/fonts.css'), 'Aliki should not copy fonts.css' |
| 72 | + refute File.exist?('fonts'), 'Aliki should not copy fonts directory' |
| 73 | + end |
| 74 | + |
| 75 | + # Aliki-specific: verify version query strings on asset references |
| 76 | + def test_asset_version_query_strings |
| 77 | + @g.generate |
| 78 | + |
| 79 | + content = File.binread('index.html') |
| 80 | + |
| 81 | + # CSS should have version query string |
| 82 | + assert_match %r{css/rdoc\.css\?v=#{Regexp.escape(RDoc::VERSION)}}, content |
| 83 | + |
| 84 | + # JS files should have version query strings |
| 85 | + assert_match %r{js/aliki\.js\?v=#{Regexp.escape(RDoc::VERSION)}}, content |
| 86 | + assert_match %r{js/search\.js\?v=#{Regexp.escape(RDoc::VERSION)}}, content |
| 87 | + assert_match %r{js/theme-toggle\.js\?v=#{Regexp.escape(RDoc::VERSION)}}, content |
| 88 | + end |
| 89 | + |
| 90 | + def test_open_graph_meta_tags_for_index |
| 91 | + @options.title = "My Ruby Project" |
| 92 | + @g.generate |
| 93 | + |
| 94 | + content = File.binread('index.html') |
| 95 | + |
| 96 | + assert_match %r{<meta property="og:type" content="website">}, content |
| 97 | + assert_match %r{<meta property="og:title" content="My Ruby Project">}, content |
| 98 | + assert_match %r{<meta\s+property="og:description"\s+content="API documentation for My Ruby Project}m, content |
| 99 | + end |
| 100 | + |
| 101 | + def test_open_graph_meta_tags_for_class |
| 102 | + top_level = @store.add_file 'file.rb' |
| 103 | + top_level.add_class @klass.class, @klass.name |
| 104 | + @klass.add_comment "A useful class for doing things.", top_level |
| 105 | + |
| 106 | + @g.generate |
| 107 | + |
| 108 | + content = File.binread('Klass.html') |
| 109 | + |
| 110 | + assert_match %r{<meta property="og:title" content=}, content |
| 111 | + assert_match %r{<meta property="og:description" content="A useful class for doing things\.">}, content |
| 112 | + end |
| 113 | + |
| 114 | + # Aliki-specific: Twitter meta tags |
| 115 | + def test_twitter_meta_tags_for_index |
| 116 | + @options.title = "My Ruby Project" |
| 117 | + @g.generate |
| 118 | + |
| 119 | + content = File.binread('index.html') |
| 120 | + |
| 121 | + assert_match %r{<meta name="twitter:card" content="summary">}, content |
| 122 | + assert_match %r{<meta name="twitter:title" content="My Ruby Project">}, content |
| 123 | + assert_match %r{<meta\s+name="twitter:description"\s+content="API documentation for My Ruby Project}m, content |
| 124 | + end |
| 125 | + |
| 126 | + def test_twitter_meta_tags_for_class |
| 127 | + top_level = @store.add_file 'file.rb' |
| 128 | + top_level.add_class @klass.class, @klass.name |
| 129 | + @klass.add_comment "A useful class for doing things.", top_level |
| 130 | + |
| 131 | + @g.generate |
| 132 | + |
| 133 | + content = File.binread('Klass.html') |
| 134 | + |
| 135 | + assert_match %r{<meta name="twitter:card" content="summary">}, content |
| 136 | + assert_match %r{<meta name="twitter:description" content="A useful class for doing things\.">}, content |
| 137 | + end |
| 138 | + |
| 139 | + def test_meta_tags_multiline_format |
| 140 | + top_level = @store.add_file 'file.rb' |
| 141 | + top_level.add_class @klass.class, @klass.name |
| 142 | + inner = @klass.add_class RDoc::NormalClass, 'Inner' |
| 143 | + inner.add_comment "This is a normal class.", top_level |
| 144 | + |
| 145 | + @g.generate |
| 146 | + |
| 147 | + content = File.binread('Klass/Inner.html') |
| 148 | + |
| 149 | + # Aliki formats meta tags across multiple lines |
| 150 | + assert_match %r{name="keywords"\s+content="ruby,class,Klass::Inner"}m, content |
| 151 | + assert_match %r{name="description"\s+content="class Klass::Inner: This is a normal class\."}m, content |
| 152 | + end |
| 153 | + |
| 154 | + def test_template_stylesheets_with_version |
| 155 | + css = Tempfile.create(%W[custom .css], Dir.mktmpdir('tmp', '.')) |
| 156 | + File.write(css, '') |
| 157 | + css.close |
| 158 | + base = File.basename(css) |
| 159 | + |
| 160 | + @options.template_stylesheets << css |
| 161 | + |
| 162 | + @g.generate |
| 163 | + |
| 164 | + assert_file base |
| 165 | + # Aliki includes version in query string for custom stylesheets too |
| 166 | + assert_match %r{href="\./#{Regexp.escape(base)}\?v=#{Regexp.escape(RDoc::VERSION)}"}, File.binread('index.html') |
| 167 | + end |
| 168 | + |
| 169 | + def test_generated_method_with_html_tag_yield_escapes_xss |
| 170 | + top_level = @store.add_file 'file.rb' |
| 171 | + top_level.add_class @klass.class, @klass.name |
| 172 | + |
| 173 | + @g.generate |
| 174 | + |
| 175 | + content = File.binread('Klass.html') |
| 176 | + |
| 177 | + # Script tags in yield params should be escaped |
| 178 | + assert_match %r{%<<script>alert\("atui"\)</script>>}, content |
| 179 | + refute_match %r{<script>alert\("atui"\)</script>}, content |
| 180 | + end |
| 181 | + |
| 182 | + def test_title_escape_prevents_xss |
| 183 | + @options.title = '<script>alert("xss")</script>' |
| 184 | + @g.generate |
| 185 | + |
| 186 | + content = File.binread('index.html') |
| 187 | + |
| 188 | + # Title should be HTML escaped |
| 189 | + assert_match %r{<title><script>alert\("xss"\)</script></title>}, content |
| 190 | + refute_match %r{<title><script>alert}, content |
| 191 | + end |
| 192 | + |
| 193 | + def test_generate |
| 194 | + @klass.add_class RDoc::NormalClass, 'Inner' |
| 195 | + @klass.add_comment "Test class documentation", @top_level |
| 196 | + |
| 197 | + @g.generate |
| 198 | + |
| 199 | + # Core HTML files |
| 200 | + assert_file 'index.html' |
| 201 | + assert_file 'Klass.html' |
| 202 | + assert_file 'Klass/Inner.html' |
| 203 | + |
| 204 | + # Aliki assets |
| 205 | + assert_file 'js/search_index.js' |
| 206 | + assert_file 'css/rdoc.css' |
| 207 | + assert_file 'js/aliki.js' |
| 208 | + |
| 209 | + # Verify HTML structure |
| 210 | + index = File.binread('index.html') |
| 211 | + assert_match %r{<html lang="en">}, index |
| 212 | + assert_match %r{<body role="document"}, index |
| 213 | + assert_match %r{<nav id="navigation" role="navigation">}, index |
| 214 | + assert_match %r{<main role="main">}, index |
| 215 | + end |
| 216 | + |
| 217 | + def test_canonical_url |
| 218 | + @klass.add_class RDoc::NormalClass, 'Inner' |
| 219 | + @store.options.canonical_root = @options.canonical_root = "https://example.com/docs/" |
| 220 | + @g.generate |
| 221 | + |
| 222 | + index_content = File.binread('index.html') |
| 223 | + assert_include index_content, '<link rel="canonical" href="https://example.com/docs/">' |
| 224 | + |
| 225 | + # Open Graph should also include canonical URL |
| 226 | + assert_match %r{<meta property="og:url" content="https://example\.com/docs/">}, index_content |
| 227 | + |
| 228 | + inner_content = File.binread('Klass/Inner.html') |
| 229 | + assert_include inner_content, '<link rel="canonical" href="https://example.com/docs/Klass/Inner.html">' |
| 230 | + end |
| 231 | + |
| 232 | + def test_dry_run_creates_no_files |
| 233 | + @g.dry_run = true |
| 234 | + |
| 235 | + @g.generate |
| 236 | + |
| 237 | + refute_file 'index.html' |
| 238 | + refute_file 'css/rdoc.css' |
| 239 | + refute_file 'js/aliki.js' |
| 240 | + end |
| 241 | + |
| 242 | + # Test locale affects html lang attribute |
| 243 | + def test_html_lang_from_locale |
| 244 | + @options.locale = RDoc::I18n::Locale.new 'ja' |
| 245 | + @g.generate |
| 246 | + |
| 247 | + content = File.binread('index.html') |
| 248 | + assert_include content, '<html lang="ja">' |
| 249 | + end |
| 250 | +end |
0 commit comments