#!/usr/bin/env perl # Drives the post type wizard in a real browser, against a running tCMS. # # The "Start from" dropdown is two halves which nothing below the browser can # check actually meet: post_wizard() writes every type that exists into the page # as JSON, and fillFromType() in post_wizard.js applies the one you picked to the # form. A unit test can prove the JSON is right and still tell you nothing about # what an admin sees, and what an admin sees is where every bug in this feature # has lived so far. # # What is asserted is the state of the form after picking a type: which canned # boxes are ticked, which body form is selected, and which rows turn up under # Custom Fields. The expectations below are the stock post types, read off the # templates in www/templates/html/components/forms -- so a theme which overrides # one of them will make this fail, which is why it is opt-in. # # Needs a running server and an admin to log in as, and skips without them: # # TCMS_TEST_URL=http://localhost:5000 \ # TCMS_TEST_USER=someadmin TCMS_TEST_PASS=hunter2 \ # prove -Ilib t/Trog-Routes-HTML-post-wizard-browser.t # # playwright_server needs its node deps (uuid, playwright, express) somewhere # node resolves them from -- its own directory, /usr/local/lib/node_modules, or # NODE_PATH. A global install under nvm is not one of those places. use 5.040; use re '/aasx'; use warnings FATAL => 'all'; use FindBin::libs; use Test::More; my $URL = $ENV{TCMS_TEST_URL} || 'http://localhost:5000'; my $USER = $ENV{TCMS_TEST_USER}; my $PASS = $ENV{TCMS_TEST_PASS}; my $TYPE = $ENV{TCMS_TEST_BROWSER} || 'firefox'; plan skip_all => 'set TCMS_TEST_USER and TCMS_TEST_PASS to run the browser tests' unless $USER && $PASS; # Playwright checks its node deps in a BEGIN, so this is a require rather than a # use: a kit that cannot start the server should skip rather than fail to # compile. eval { require Playwright; 1 } or plan skip_all => "Playwright will not load: $@"; # Every stock post type, as the wizard ought to read it back. 'boxes' is every # checkbox the wizard offers; 'fields' is what should be left under Custom # Fields once the canned blocks have been accounted for. my %EXPECT = ( 'blog.tx' => { body => 'form_common.tx', fields => [], boxes => { wrapper => 1, inc_post_title => 1, inc_post_tags => 1, inc_preview => 1, inc_tags => 1, inc_aliases => 1, inc_attachments => 0 }, }, # The multi-page one: reopening it used to hand back form_common.tx, so # saving it again silently turned a slide deck into a single page. 'presentation.tx' => { body => 'form_multi.tx', fields => ['video_href'], boxes => { wrapper => 1, inc_post_title => 1, inc_post_tags => 1, inc_preview => 1, inc_tags => 1, inc_aliases => 1, inc_attachments => 1 }, }, # No wrapper -- it opens with its own markup so it can tile -- and its avatar # upload is its own rather than preview.tx, so preview really is a field of # this type and should still be offered as one. 'profile.tx' => { body => 'form_common.tx', fields => [qw{contact_email display_name preview preview_file user_acls username wallpaper wallpaper_file}], boxes => { wrapper => 0, inc_post_title => 1, inc_post_tags => 0, inc_preview => 0, inc_tags => 0, inc_aliases => 1, inc_attachments => 0 }, }, # A datasource type: built rather than written, so it has no editor at all, # and the preview it declares is a console capture of its own. 'guests.tx' => { body => 'form_common.tx', fields => [qw{preview hypervisors}], boxes => { wrapper => 1, inc_post_title => 0, inc_post_tags => 0, inc_preview => 0, inc_tags => 0, inc_aliases => 0, inc_attachments => 0 }, }, ); my @BOXES = qw{wrapper inc_post_title inc_post_tags inc_preview inc_tags inc_aliases inc_attachments}; # Locators throughout rather than select(): a select() hands back a handle to # the element as it was at that moment, and the login page settles after # 'networkidle' often enough that filling one raced with the DOM it came from. # A locator resolves when it is acted on instead. my $handle = Playwright->new(); my $browser = $handle->launch( headless => 1, type => $TYPE ); my $page = $browser->newPage(); END { eval { $page->close() } if $page; eval { $browser->close() } if $browser; eval { $handle->quit() } if $handle; } sub field_names { return $page->evaluate('return Array.from(document.querySelectorAll("#wizard-params input[name=param_name]")).map(function (i) { return i.value; });'); } subtest 'an admin can reach the wizard' => sub { # Posted rather than typed into the login page. The session lands in the # page's own cookie jar either way, and this keeps the test off a page it is # not about: on a successful login jsalert.tx schedules a window.location to # $to half a second after DOMContentLoaded, so the login form navigates out # from under anything still filling it in. my $res = $page->request->fetch( "$URL/auth", { method => 'POST', form => { app => 'login', to => '', username => $USER, password => $PASS }, } ); is( $res->status(), 200, "logged in at $URL" ) or BAIL_OUT("no tCMS answering at $URL, or those credentials are wrong"); $res = $page->goto( "$URL/admin/wyzzerdd", { waitUntil => 'networkidle' } ); is( $res->status(), 200, 'the wizard renders' ); is( $page->locator('#wizard-existing')->count(), 1, "the 'Start from' dropdown is there" ) or BAIL_OUT("not logged in as an admin -- check TCMS_TEST_USER"); }; foreach my $form ( sort keys(%EXPECT) ) { subtest "picking $form fills the form in the way that type was built" => sub { my $expect = $EXPECT{$form}; # Back to blank between types, so nothing here can pass on what the # previous type left in the form rather than on what this one says. $page->locator('#wizard-existing')->selectOption( { value => '' } ); is_deeply( field_names(), [''], 'a blank pick leaves one empty custom field row' ); $page->locator('#wizard-existing')->selectOption( { value => $form } ); foreach my $box (@BOXES) { my $want = $expect->{boxes}{$box}; my $got = $page->locator(qq{#postTypeWizard [name="$box"]})->isChecked() ? 1 : 0; is( $got, $want, "$box is " . ( $want ? 'ticked' : 'clear' ) ); } is( $page->locator('#postTypeWizard [name="body_form"]')->inputValue(), $expect->{body}, 'the body form is the one the type carries' ); # Only the named rows are asserted: fillFromType() always leaves an # empty one behind to type the next field into, and a type with no # custom fields at all gets that row and nothing else. is_deeply( [ sort grep { $_ } @{ field_names() } ], [ sort @{ $expect->{fields} } ], 'and only the fields somebody typed in are offered as custom ones' ); # Saving under the same name is the point of picking one, and the save # is refused outright without this. ok( $page->locator('#postTypeWizard [name="overwrite"]')->isChecked(), 'overwrite is ticked for you' ); }; } done_testing();