Trog-Component-page.t 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. #!/usr/bin/env perl
  2. use 5.040;
  3. use re '/aasx';
  4. use warnings FATAL => 'all';
  5. use Test::More;
  6. use Test::Fatal qw{exception};
  7. use FindBin;
  8. use FindBin::libs;
  9. # The components render real templates through the real renderer, which resolves
  10. # them relative to the tCMS root. Xslate also computes its cache dir from $HOME
  11. # at load time, so point that somewhere disposable before it is loaded.
  12. my $ROOT;
  13. BEGIN {
  14. require File::Temp;
  15. $ROOT = File::Temp::tempdir( 'tcms-component-XXXXXX', TMPDIR => 1, CLEANUP => 1 );
  16. $ENV{HOME} = $ROOT;
  17. chdir("$FindBin::Bin/..") or die "could not chdir to the tCMS root: $!";
  18. }
  19. require_ok('Trog::Component') or BAIL_OUT("Can't find SUT");
  20. require_ok('Trog::Component::Header') or BAIL_OUT("Can't find SUT");
  21. require_ok('Trog::Component::PostHeader') or BAIL_OUT("Can't find SUT");
  22. subtest 'the furniture with no arguments' => sub {
  23. # These six are empty or a stock placeholder, and read no variables at all.
  24. # The test is that each resolves its own template and comes back with a
  25. # string rather than dying -- which is the whole of what index.tx asks of it.
  26. foreach my $name (qw{HtmlTitle MidTitle TopBar LeftBar RightBar FootBar Footer}) {
  27. my $out = Trog::Component::component($name);
  28. is( exception { Trog::Component::component($name) }, undef, "$name renders" );
  29. isa_ok( $out, 'Text::Xslate::Type::Raw', "$name comes back marked raw" );
  30. }
  31. like( Trog::Component::component('HtmlTitle'), qr/tCMS/, "HtmlTitle is the site title" );
  32. like( Trog::Component::component('Footer'), qr{</html>}i, "Footer closes the document" );
  33. };
  34. subtest 'Header' => sub {
  35. my $out = Trog::Component::component(
  36. 'Header',
  37. {
  38. title => 'A Page',
  39. route => '/somewhere',
  40. stylesheets => ['/styles/screen.css'],
  41. scripts => ['/scripts/post.js'],
  42. }
  43. );
  44. like( $out, qr/<!doctype[ ]html>/i, "the doctype is emitted by default" );
  45. like( $out, qr{<title>A[ ]Page</title>}, "the title lands" );
  46. like(
  47. Trog::Component::component( 'Header', { title => '<b>x</b>' } ),
  48. qr{<title>&lt;b&gt;x&lt;/b&gt;</title>},
  49. "and is text, not markup"
  50. );
  51. like( $out, qr{href="/styles/screen\.css"}, "stylesheets land" );
  52. like( $out, qr{src="/scripts/post\.js"}, "scripts land" );
  53. like( $out, qr{lang="en-US"}, "lang defaults rather than rendering empty" );
  54. like( $out, qr{href="/somewhere\?format=rss"}, "the rss link uses the route" );
  55. # rss_style() renders into an XSL document, so the doctype has to go.
  56. my $xsl = Trog::Component::component( 'Header', { title => 'RSS', no_doctype => 1 } );
  57. unlike( $xsl, qr/<!doctype/i, "no_doctype suppresses it" );
  58. like(
  59. Trog::Component::component( 'Header', { title => 'RSS', title_select => 'rss/channel/title', no_doctype => 1 } ),
  60. qr{<title><xsl:value-of[ ]select="rss/channel/title"/></title>},
  61. "and title_select makes the title an XSL value"
  62. );
  63. # It also has no stylesheets or scripts to give, and must not invent any.
  64. unlike( $xsl, qr/rel="stylesheet"[ ]type="text\/css"[ ]href="\/styles\/screen/, "no stylesheets are conjured up" );
  65. like(
  66. exception { Trog::Component::component( 'Header', { bogus => 1 } ) },
  67. qr/takes[ ]no[ ]argument[ ]named[ ]bogus/,
  68. "an argument it does not take is fatal"
  69. );
  70. # header.tx marks meta_tags raw, so social markup has to survive as markup.
  71. my $social = Trog::Component::component( 'Header', { meta_tags => '<meta name="twitter:card" />' } );
  72. like( $social, qr{<meta[ ]name="twitter:card"[ ]/>}, "meta_tags stay markup" );
  73. };
  74. subtest 'CategoryBar' => sub {
  75. my $categories = [ { local_href => '/series/blog', title => 'Blog' } ];
  76. my $out = Trog::Component::component( 'CategoryBar', { categories => $categories } );
  77. like( $out, qr{href="/series/blog"}, "an anonymous reader gets the plain link" );
  78. my $secure = Trog::Component::component( 'CategoryBar', { categories => $categories, user => 'someone' } );
  79. like( $secure, qr{href="/secure/series/blog"}, "a logged in reader gets the secure link" );
  80. is( exception { Trog::Component::component('CategoryBar') }, undef, "no categories is not fatal" );
  81. };
  82. subtest 'PostHeader and PostFooter validate the name' => sub {
  83. # The name comes out of post data, and before this it went straight into a
  84. # path with nothing checking it.
  85. foreach my $bad (qw{../../etc/passwd headers/../../../etc/passwd nope.tx about_header}) {
  86. like(
  87. exception { Trog::Component::component( 'PostHeader', { name => $bad } ) },
  88. qr/No[ ]such[ ]header/,
  89. "'$bad' is refused"
  90. );
  91. }
  92. # An empty name is how a post with no header asks, so it must not be fatal.
  93. is( Trog::Component::component( 'PostHeader', { name => '' } ), '', "no name renders nothing" );
  94. is( Trog::Component::component('PostHeader'), '', "a missing name renders nothing" );
  95. is( Trog::Component::component( 'PostFooter', { name => undef } ), '', "an undef name renders nothing" );
  96. # Whatever the edit form offers has to actually render.
  97. my $available = Trog::Component::PostHeader::available();
  98. ok( scalar(@$available), "there is at least one stock header to try" ) or return;
  99. foreach my $name (@$available) {
  100. is( exception { Trog::Component::component( 'PostHeader', { name => $name } ) }, undef, "$name renders" );
  101. }
  102. # available() is the same list the series edit form is built from.
  103. is_deeply(
  104. $available,
  105. Trog::Themes::themed_templates_in_dir( 'headers', 'text/html', 1 ),
  106. "available() is the list the edit form offers"
  107. );
  108. };
  109. subtest 'the whole page still composes' => sub {
  110. require Trog::Renderer::Base;
  111. # index.tx is a page, not a component, so it resolves in www/templates/html
  112. # rather than the components dir and comes back as a PSGI triplet.
  113. my $psgi = eval {
  114. Trog::Renderer::Base::render(
  115. template => 'index.tx',
  116. contenttype => 'text/html',
  117. code => 200,
  118. data => {
  119. title => 'Composed',
  120. route => '/',
  121. content => '<p>the body</p>',
  122. categories => [ { local_href => '/series/blog', title => 'Blog' } ],
  123. stylesheets => ['/styles/screen.css'],
  124. print_styles => [],
  125. scripts => [],
  126. ( map { $_ => undef } qw{author default_tags embed failure is_admin lang message meta_desc meta_tags search_help search_lang show_madeby to user user_class} ),
  127. },
  128. );
  129. };
  130. my $err = $@;
  131. is( $err, '', "index.tx renders" ) or diag($err);
  132. my $body = ref $psgi eq 'ARRAY' ? $psgi->[2][0] : undef;
  133. like( $body, qr/<!doctype[ ]html>/i, "Header came through" );
  134. like( $body, qr{<title>Composed</title>}, "the title came through" );
  135. like( $body, qr{<p>the[ ]body</p>}, "the content is still the content" );
  136. like( $body, qr{href="/series/blog"}, "CategoryBar came through" );
  137. like( $body, qr{</html>}, "Footer came through" );
  138. # The furniture divs are the bars' sockets; they must still be there even
  139. # though the stock bars render placeholders.
  140. like( $body, qr{id="leftbar"}, "the left bar socket is intact" );
  141. like( $body, qr{id="footbar"}, "the footer bar socket is intact" );
  142. };
  143. done_testing();