Trog-Component-page.t 6.7 KB

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