Trog-Routes-HTML-post-wizard-browser.t 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. #!/usr/bin/env perl
  2. # Drives the post type wizard in a real browser, against a running tCMS.
  3. #
  4. # The "Start from" dropdown is two halves which nothing below the browser can
  5. # check actually meet: post_wizard() writes every type that exists into the page
  6. # as JSON, and fillFromType() in post_wizard.js applies the one you picked to the
  7. # form. A unit test can prove the JSON is right and still tell you nothing about
  8. # what an admin sees, and what an admin sees is where every bug in this feature
  9. # has lived so far.
  10. #
  11. # What is asserted is the state of the form after picking a type: which canned
  12. # boxes are ticked, which body form is selected, and which rows turn up under
  13. # Custom Fields. The expectations below are the stock post types, read off the
  14. # templates in www/templates/html/components/forms -- so a theme which overrides
  15. # one of them will make this fail, which is why it is opt-in.
  16. #
  17. # Needs a running server and an admin to log in as, and skips without them:
  18. #
  19. # TCMS_TEST_URL=http://localhost:5000 \
  20. # TCMS_TEST_USER=someadmin TCMS_TEST_PASS=hunter2 \
  21. # prove -Ilib t/Trog-Routes-HTML-post-wizard-browser.t
  22. #
  23. # playwright_server needs its node deps (uuid, playwright, express) somewhere
  24. # node resolves them from -- its own directory, /usr/local/lib/node_modules, or
  25. # NODE_PATH. A global install under nvm is not one of those places.
  26. use strict;
  27. use warnings;
  28. use FindBin;
  29. use lib "$FindBin::Bin/../lib";
  30. use Test::More;
  31. my $URL = $ENV{TCMS_TEST_URL} || 'http://localhost:5000';
  32. my $USER = $ENV{TCMS_TEST_USER};
  33. my $PASS = $ENV{TCMS_TEST_PASS};
  34. my $TYPE = $ENV{TCMS_TEST_BROWSER} || 'firefox';
  35. plan skip_all => 'set TCMS_TEST_USER and TCMS_TEST_PASS to run the browser tests' unless $USER && $PASS;
  36. # Playwright checks its node deps in a BEGIN, so this is a require rather than a
  37. # use: a kit that cannot start the server should skip rather than fail to
  38. # compile.
  39. eval { require Playwright; 1 } or plan skip_all => "Playwright will not load: $@";
  40. # Every stock post type, as the wizard ought to read it back. 'boxes' is every
  41. # checkbox the wizard offers; 'fields' is what should be left under Custom
  42. # Fields once the canned blocks have been accounted for.
  43. my %EXPECT = (
  44. 'blog.tx' => {
  45. body => 'form_common.tx',
  46. fields => [],
  47. boxes => { wrapper => 1, inc_post_title => 1, inc_post_tags => 1, inc_preview => 1, inc_tags => 1, inc_aliases => 1, inc_attachments => 0 },
  48. },
  49. # The multi-page one: reopening it used to hand back form_common.tx, so
  50. # saving it again silently turned a slide deck into a single page.
  51. 'presentation.tx' => {
  52. body => 'form_multi.tx',
  53. fields => ['video_href'],
  54. boxes => { wrapper => 1, inc_post_title => 1, inc_post_tags => 1, inc_preview => 1, inc_tags => 1, inc_aliases => 1, inc_attachments => 1 },
  55. },
  56. # No wrapper -- it opens with its own markup so it can tile -- and its avatar
  57. # upload is its own rather than preview.tx, so preview really is a field of
  58. # this type and should still be offered as one.
  59. 'profile.tx' => {
  60. body => 'form_common.tx',
  61. fields => [qw{contact_email display_name preview preview_file user_acls username wallpaper wallpaper_file}],
  62. boxes => { wrapper => 0, inc_post_title => 1, inc_post_tags => 0, inc_preview => 0, inc_tags => 0, inc_aliases => 1, inc_attachments => 0 },
  63. },
  64. # A datasource type: built rather than written, so it has no editor at all,
  65. # and the preview it declares is a console capture of its own.
  66. 'guests.tx' => {
  67. body => 'form_common.tx',
  68. fields => [qw{preview hypervisors}],
  69. boxes => { wrapper => 1, inc_post_title => 0, inc_post_tags => 0, inc_preview => 0, inc_tags => 0, inc_aliases => 0, inc_attachments => 0 },
  70. },
  71. );
  72. my @BOXES = qw{wrapper inc_post_title inc_post_tags inc_preview inc_tags inc_aliases inc_attachments};
  73. # Locators throughout rather than select(): a select() hands back a handle to
  74. # the element as it was at that moment, and the login page settles after
  75. # 'networkidle' often enough that filling one raced with the DOM it came from.
  76. # A locator resolves when it is acted on instead.
  77. my $handle = Playwright->new();
  78. my $browser = $handle->launch( headless => 1, type => $TYPE );
  79. my $page = $browser->newPage();
  80. END {
  81. eval { $page->close() } if $page;
  82. eval { $browser->close() } if $browser;
  83. eval { $handle->quit() } if $handle;
  84. }
  85. sub field_names {
  86. return $page->evaluate('return Array.from(document.querySelectorAll("#wizard-params input[name=param_name]")).map(function (i) { return i.value; });');
  87. }
  88. subtest 'an admin can reach the wizard' => sub {
  89. # Posted rather than typed into the login page. The session lands in the
  90. # page's own cookie jar either way, and this keeps the test off a page it is
  91. # not about: on a successful login jsalert.tx schedules a window.location to
  92. # $to half a second after DOMContentLoaded, so the login form navigates out
  93. # from under anything still filling it in.
  94. my $res = $page->request->fetch(
  95. "$URL/auth",
  96. {
  97. method => 'POST',
  98. form => { app => 'login', to => '', username => $USER, password => $PASS },
  99. }
  100. );
  101. is( $res->status(), 200, "logged in at $URL" ) or BAIL_OUT("no tCMS answering at $URL, or those credentials are wrong");
  102. $res = $page->goto( "$URL/admin/wyzzerdd", { waitUntil => 'networkidle' } );
  103. is( $res->status(), 200, 'the wizard renders' );
  104. is( $page->locator('#wizard-existing')->count(), 1, "the 'Start from' dropdown is there" )
  105. or BAIL_OUT("not logged in as an admin -- check TCMS_TEST_USER");
  106. };
  107. foreach my $form ( sort keys(%EXPECT) ) {
  108. subtest "picking $form fills the form in the way that type was built" => sub {
  109. my $expect = $EXPECT{$form};
  110. # Back to blank between types, so nothing here can pass on what the
  111. # previous type left in the form rather than on what this one says.
  112. $page->locator('#wizard-existing')->selectOption( { value => '' } );
  113. is_deeply( field_names(), [''], 'a blank pick leaves one empty custom field row' );
  114. $page->locator('#wizard-existing')->selectOption( { value => $form } );
  115. foreach my $box (@BOXES) {
  116. my $want = $expect->{boxes}{$box};
  117. my $got = $page->locator(qq{#postTypeWizard [name="$box"]})->isChecked() ? 1 : 0;
  118. is( $got, $want, "$box is " . ( $want ? 'ticked' : 'clear' ) );
  119. }
  120. is( $page->locator('#postTypeWizard [name="body_form"]')->inputValue(), $expect->{body}, 'the body form is the one the type carries' );
  121. # Only the named rows are asserted: fillFromType() always leaves an
  122. # empty one behind to type the next field into, and a type with no
  123. # custom fields at all gets that row and nothing else.
  124. is_deeply(
  125. [ sort grep { length } @{ field_names() } ],
  126. [ sort @{ $expect->{fields} } ],
  127. 'and only the fields somebody typed in are offered as custom ones'
  128. );
  129. # Saving under the same name is the point of picking one, and the save
  130. # is refused outright without this.
  131. ok( $page->locator('#postTypeWizard [name="overwrite"]')->isChecked(), 'overwrite is ticked for you' );
  132. };
  133. }
  134. done_testing();