This commit is contained in:
2024-10-14 00:08:40 +02:00
parent dbfba56f66
commit 1462d52e13
4572 changed files with 2658864 additions and 0 deletions

View File

@@ -0,0 +1,574 @@
# --
# Copyright (C) 2001-2019 OTRS AG, https://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (GPL). If you
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
# --
package Kernel::System::UnitTest::Driver;
use strict;
use warnings;
use Storable();
use Term::ANSIColor();
# UnitTest helper must be loaded to override the builtin time functions!
use Kernel::System::UnitTest::Helper;
use Kernel::System::VariableCheck qw(DataIsDifferent);
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::Log',
'Kernel::System::Main',
);
=head1 NAME
Kernel::System::UnitTest::Driver - unit test file execution wrapper
=head1 PUBLIC INTERFACE
=head2 new()
create unit test driver object. Do not use it directly, instead use:
my $Driver = $Kernel::OM->Create(
'Kernel::System::UnitTest::Driver',
ObjectParams => {
Verbose => $Self->{Verbose},
ANSI => $Self->{ANSI},
},
);
=cut
sub new {
my ( $Type, %Param ) = @_;
# allocate new hash for object
my $Self = {};
bless( $Self, $Type );
$Self->{ANSI} = $Param{ANSI};
$Self->{Verbose} = $Param{Verbose};
# We use an output buffering mechanism if Verbose is not set. Only failed tests will be output in this case.
# Make sure stuff is always flushed to keep it in the right order.
*STDOUT->autoflush(1);
*STDERR->autoflush(1);
$Self->{OriginalSTDOUT} = *STDOUT;
$Self->{OriginalSTDOUT}->autoflush(1);
$Self->{OutputBuffer} = '';
# Report results via file.
$Self->{ResultDataFile} = $Kernel::OM->Get('Kernel::Config')->Get('Home') . '/var/tmp/UnitTest.dump';
unlink $Self->{ResultDataFile}; # purge if exists
return $Self;
}
=head2 Run()
executes a single unit test file and provides it with an empty environment (fresh C<ObjectManager> instance).
This method assumes that it runs in a dedicated child process just for this one unit test.
This process forking is done in L<Kernel::System::UnitTest>, which creates one child process per test file.
All results will be collected and written to a C<var/tmp/UnitTest.dump> file that the main process will
load to collect all results.
=cut
sub Run {
my ( $Self, %Param ) = @_;
my $File = $Param{File};
my $UnitTestFile = $Kernel::OM->Get('Kernel::System::Main')->FileRead(
Location => $File,
);
if ( !$UnitTestFile ) {
$Self->True( 0, "ERROR: $!: $File" );
print STDERR "ERROR: $!: $File\n";
$Self->_SaveResults();
return;
}
print "+-------------------------------------------------------------------+\n";
print "$File:\n";
print "+-------------------------------------------------------------------+\n";
# Use non-overridden time() function.
my $FileStartTime = CORE::time; ## no critic
# Create a new scope to be sure to destroy local object of the test files.
{
# Make sure every UT uses its own clean environment.
## nofilter(TidyAll::Plugin::OTRS::Perl::ObjectManagerCreation)
local $Kernel::OM = Kernel::System::ObjectManager->new(
'Kernel::System::Log' => {
LogPrefix => 'OTRS-otrs.UnitTest',
},
);
# Provide $Self as 'Kernel::System::UnitTest' for convenience.
$Kernel::OM->ObjectInstanceRegister(
Package => 'Kernel::System::UnitTest::Driver',
Object => $Self,
Dependencies => [],
);
$Self->{OutputBuffer} = '';
local *STDOUT = *STDOUT;
local *STDERR = *STDERR;
if ( !$Self->{Verbose} ) {
undef *STDOUT;
undef *STDERR;
open STDOUT, '>:utf8', \$Self->{OutputBuffer}; ## no critic
open STDERR, '>:utf8', \$Self->{OutputBuffer}; ## no critic
}
# HERE the actual tests are run.
my $TestSuccess = eval ${$UnitTestFile}; ## no critic
if ( !$TestSuccess ) {
if ($@) {
$Self->True( 0, "ERROR: Error in $File: $@" );
}
else {
$Self->True( 0, "ERROR: $File did not return a true value." );
}
}
}
# Use non-overridden time() function.
$Self->{ResultData}->{Duration} = CORE::time - $FileStartTime; ## no critic
if ( $Self->{SeleniumData} ) {
$Self->{ResultData}->{SeleniumData} = $Self->{SeleniumData};
}
print { $Self->{OriginalSTDOUT} } "\n";
return $Self->_SaveResults();
}
=head2 True()
test for a scalar value that evaluates to true.
Send a scalar value to this function along with the test's name:
$UnitTestObject->True(1, 'Test Name');
$UnitTestObject->True($ParamA, 'Test Name');
Internally, the function receives this value and evaluates it to see
if it's true, returning 1 in this case or undef, otherwise.
my $TrueResult = $UnitTestObject->True(
$TestValue,
'Test Name',
);
=cut
sub True {
my ( $Self, $True, $Name ) = @_;
if ( !$Name ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need Name! E. g. True(\$A, \'Test Name\')!'
);
$Self->_Print( 0, 'ERROR: Need Name! E. g. True(\$A, \'Test Name\')' );
return;
}
if ($True) {
$Self->_Print( 1, $Name );
return 1;
}
else {
$Self->_Print( 0, $Name );
return;
}
}
=head2 False()
test for a scalar value that evaluates to false.
It has the same interface as L</True()>, but tests
for a false value instead.
=cut
sub False {
my ( $Self, $False, $Name ) = @_;
if ( !$Name ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need Name! E. g. False(\$A, \'Test Name\')!'
);
$Self->_Print( 0, 'ERROR: Need Name! E. g. False(\$A, \'Test Name\')' );
return;
}
if ( !$False ) {
$Self->_Print( 1, $Name );
return 1;
}
else {
$Self->_Print( 0, $Name );
return;
}
}
=head2 Is()
compares two scalar values for equality.
To this function you must send a pair of scalar values to compare them,
and the name that the test will take, this is done as shown in the examples
below.
$UnitTestObject->Is($A, $B, 'Test Name');
Returns 1 if the values were equal, or undef otherwise.
my $IsResult = $UnitTestObject->Is(
$ValueFromFunction, # test data
1, # expected value
'Test Name',
);
=cut
sub Is {
my ( $Self, $Test, $ShouldBe, $Name ) = @_;
if ( !$Name ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need Name! E. g. Is(\$A, \$B, \'Test Name\')!'
);
$Self->_Print( 0, 'ERROR: Need Name! E. g. Is(\$A, \$B, \'Test Name\')' );
return;
}
if ( !defined $Test && !defined $ShouldBe ) {
$Self->_Print( 1, "$Name (is 'undef')" );
return 1;
}
elsif ( !defined $Test && defined $ShouldBe ) {
$Self->_Print( 0, "$Name (is 'undef' should be '$ShouldBe')" );
return;
}
elsif ( defined $Test && !defined $ShouldBe ) {
$Self->_Print( 0, "$Name (is '$Test' should be 'undef')" );
return;
}
elsif ( $Test eq $ShouldBe ) {
$Self->_Print( 1, "$Name (is '$ShouldBe')" );
return 1;
}
else {
$Self->_Print( 0, "$Name (is '$Test' should be '$ShouldBe')" );
return;
}
}
=head2 IsNot()
compares two scalar values for inequality.
It has the same interface as L</Is()>, but tests
for inequality instead.
=cut
sub IsNot {
my ( $Self, $Test, $ShouldBe, $Name ) = @_;
if ( !$Name ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need Name! E. g. IsNot(\$A, \$B, \'Test Name\')!'
);
$Self->_Print( 0, 'ERROR: Need Name! E. g. IsNot(\$A, \$B, \'Test Name\')' );
return;
}
if ( !defined $Test && !defined $ShouldBe ) {
$Self->_Print( 0, "$Name (is 'undef')" );
return;
}
elsif ( !defined $Test && defined $ShouldBe ) {
$Self->_Print( 1, "$Name (is 'undef')" );
return 1;
}
elsif ( defined $Test && !defined $ShouldBe ) {
$Self->_Print( 1, "$Name (is '$Test')" );
return 1;
}
if ( $Test ne $ShouldBe ) {
$Self->_Print( 1, "$Name (is '$Test')" );
return 1;
}
else {
$Self->_Print( 0, "$Name (is '$Test' should not be '$ShouldBe')" );
return;
}
}
=head2 IsDeeply()
compares complex data structures for equality.
To this function you must send the references to two data structures to be compared,
and the name that the test will take, this is done as shown in the examples
below.
$UnitTestObject-> IsDeeply($ParamA, $ParamB, 'Test Name');
Where $ParamA and $ParamB must be references to a structure (scalar, list or hash).
Returns 1 if the data structures are the same, or undef otherwise.
my $IsDeeplyResult = $UnitTestObject->IsDeeply(
\%ResultHash, # test data
\%ExpectedHash, # expected value
'Dummy Test Name',
);
=cut
sub IsDeeply {
my ( $Self, $Test, $ShouldBe, $Name ) = @_;
if ( !$Name ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need Name! E. g. Is(\$A, \$B, \'Test Name\')!'
);
$Self->_Print( 0, 'ERROR: Need Name! E. g. Is(\$A, \$B, \'Test Name\')' );
return;
}
my $Diff = DataIsDifferent(
Data1 => $Test,
Data2 => $ShouldBe,
);
if ( !defined $Test && !defined $ShouldBe ) {
$Self->_Print( 1, "$Name (is 'undef')" );
return 1;
}
elsif ( !defined $Test && defined $ShouldBe ) {
$Self->_Print( 0, "$Name (is 'undef' should be defined)" );
return;
}
elsif ( defined $Test && !defined $ShouldBe ) {
$Self->_Print( 0, "$Name (is defined should be 'undef')" );
return;
}
elsif ( !$Diff ) {
$Self->_Print( 1, "$Name matches expected value" );
return 1;
}
else {
my $ShouldBeDump = $Kernel::OM->Get('Kernel::System::Main')->Dump($ShouldBe);
my $TestDump = $Kernel::OM->Get('Kernel::System::Main')->Dump($Test);
$Self->_Print( 0, "$Name (is '$TestDump' should be '$ShouldBeDump')" );
return;
}
}
=head2 IsNotDeeply()
compares two data structures for inequality.
It has the same interface as L</IsDeeply()>, but tests
for inequality instead.
=cut
sub IsNotDeeply {
my ( $Self, $Test, $ShouldBe, $Name ) = @_;
if ( !$Name ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => 'Need Name! E. g. IsNot(\$A, \$B, \'Test Name\')!'
);
$Self->_Print( 0, 'ERROR: Need Name! E. g. IsNot(\$A, \$B, \'Test Name\')' );
return;
}
my $Diff = DataIsDifferent(
Data1 => $Test,
Data2 => $ShouldBe,
);
if ( !defined $Test && !defined $ShouldBe ) {
$Self->_Print( 0, "$Name (is 'undef')" );
return;
}
elsif ( !defined $Test && defined $ShouldBe ) {
$Self->_Print( 1, "$Name (is 'undef')" );
return 1;
}
elsif ( defined $Test && !defined $ShouldBe ) {
$Self->_Print( 1, "$Name (differs from expected value)" );
return 1;
}
if ($Diff) {
$Self->_Print( 1, "$Name (The structures are not equal.)" );
return 1;
}
else {
# $Self->_Print( 0, "$Name (matches the expected value)" );
my $TestDump = $Kernel::OM->Get('Kernel::System::Main')->Dump($Test);
$Self->_Print( 0, "$Name (The structures are equal: '$TestDump')" );
return;
}
}
=head2 AttachSeleniumScreenshot()
attach a screenshot taken during Selenium error handling. These will be sent to the server
together with the test results.
$Driver->AttachSeleniumScreenshot(
Filename => $Filename,
Content => $Data # raw image data
);
=cut
sub AttachSeleniumScreenshot {
my ( $Self, %Param ) = @_;
push @{ $Self->{ResultData}->{Results}->{ $Self->{TestCount} }->{Screenshots} },
{
Filename => $Param{Filename},
Content => $Param{Content},
};
return;
}
=begin Internal:
=cut
sub _SaveResults {
my ($Self) = @_;
if ( !$Self->{ResultData} ) {
$Self->True( 0, 'No result data found.' );
}
my $Success = Storable::nstore( $Self->{ResultData}, $Self->{ResultDataFile} );
if ( !$Success ) {
print STDERR $Self->_Color( 'red', "Could not store result data in $Self->{ResultDataFile}\n" );
return 0;
}
return 1;
}
sub _Print {
my ( $Self, $ResultOk, $Message ) = @_;
$Message ||= '->>No Name!<<-';
my $ShortMessage = $Message;
if ( length $ShortMessage > 1000 && !$Self->{Verbose} ) {
$ShortMessage = substr( $ShortMessage, 0, 1000 ) . "...";
}
if ( $Self->{Verbose} || !$ResultOk ) {
print { $Self->{OriginalSTDOUT} } $Self->{OutputBuffer};
}
$Self->{OutputBuffer} = '';
$Self->{TestCount}++;
if ($ResultOk) {
if ( $Self->{Verbose} ) {
print { $Self->{OriginalSTDOUT} } " "
. $Self->_Color( 'green', "ok" )
. " $Self->{TestCount} - $ShortMessage\n";
}
else {
print { $Self->{OriginalSTDOUT} } $Self->_Color( 'green', "." );
}
$Self->{ResultData}->{TestOk}++;
return 1;
}
else {
if ( !$Self->{Verbose} ) {
print { $Self->{OriginalSTDOUT} } "\n";
}
print { $Self->{OriginalSTDOUT} } " "
. $Self->_Color( 'red', "not ok" )
. " $Self->{TestCount} - $ShortMessage\n";
$Self->{ResultData}->{TestNotOk}++;
$Self->{ResultData}->{Results}->{ $Self->{TestCount} }->{Status} = 'not ok';
$Self->{ResultData}->{Results}->{ $Self->{TestCount} }->{Message} = $Message;
my $TestFailureDetails = $Message;
$TestFailureDetails =~ s{\(.+\)$}{};
if ( length $TestFailureDetails > 200 ) {
$TestFailureDetails = substr( $TestFailureDetails, 0, 200 ) . "...";
}
# Store information about failed tests, but only if we are running in a toplevel unit test object
# that is actually processing files, and not in an embedded object that just runs individual tests.
push @{ $Self->{ResultData}->{NotOkInfo} }, sprintf "%s - %s", $Self->{TestCount},
$TestFailureDetails;
return;
}
}
=head2 _Color()
this will color the given text (see Term::ANSIColor::color()) if
ANSI output is available and active, otherwise the text stays unchanged.
my $PossiblyColoredText = $CommandObject->_Color('green', $Text);
=cut
sub _Color {
my ( $Self, $Color, $Text ) = @_;
return $Text if !$Self->{ANSI};
return Term::ANSIColor::color($Color) . $Text . Term::ANSIColor::color('reset');
}
1;
=end Internal:
=head1 TERMS AND CONDITIONS
This software is part of the OTRS project (L<https://otrs.org/>).
This software comes with ABSOLUTELY NO WARRANTY. For details, see
the enclosed file COPYING for license information (GPL). If you
did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
=cut

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,824 @@
# --
# Copyright (C) 2001-2019 OTRS AG, https://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (GPL). If you
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
# --
package Kernel::System::UnitTest::Selenium;
use strict;
use warnings;
use MIME::Base64();
use File::Path();
use File::Temp();
use Time::HiRes();
use Kernel::Config;
use Kernel::System::User;
use Kernel::System::UnitTest::Helper;
use Kernel::System::VariableCheck qw(IsArrayRefWithData);
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::AuthSession',
'Kernel::System::Log',
'Kernel::System::Main',
'Kernel::System::DateTime',
'Kernel::System::UnitTest::Driver',
'Kernel::System::UnitTest::Helper',
);
# If a test throws an exception, we'll record it here in a package variable so that we can
# take screenshots of *all* Selenium instances that are currently running on shutdown.
our $TestException;
=head1 NAME
Kernel::System::UnitTest::Selenium - run front end tests
This class inherits from Selenium::Remote::Driver. You can use
its full API (see
L<http://search.cpan.org/~aivaturi/Selenium-Remote-Driver-0.15/lib/Selenium/Remote/Driver.pm>).
Every successful Selenium command will be logged as a successful unit test.
In case of an error, an exception will be thrown that you can catch in your
unit test file and handle with C<HandleError()> in this class. It will output
a failing test result and generate a screen shot for analysis.
=head2 new()
create a selenium object to run front end tests.
To do this, you need a running C<selenium> or C<phantomjs> server.
Specify the connection details in C<Config.pm>, like this:
# For testing with Firefox until v. 47 (testing with recent FF and marionette is currently not supported):
$Self->{'SeleniumTestsConfig'} = {
remote_server_addr => 'localhost',
port => '4444',
platform => 'ANY',
browser_name => 'firefox',
extra_capabilities => {
marionette => \0, # Required to run FF 47 or older on Selenium 3+.
},
};
# For testing with Chrome/Chromium (requires installed geckodriver):
$Self->{'SeleniumTestsConfig'} = {
remote_server_addr => 'localhost',
port => '4444',
platform => 'ANY',
browser_name => 'chrome',
extra_capabilities => {
chromeOptions => {
# disable-infobars makes sure window size calculations are ok
args => [ "disable-infobars" ],
},
},
};
Then you can use the full API of L<Selenium::Remote::Driver> on this object.
=cut
sub new {
my ( $Class, %Param ) = @_;
$Param{UnitTestDriverObject} ||= $Kernel::OM->Get('Kernel::System::UnitTest::Driver');
$Param{UnitTestDriverObject}->True( 1, "Starting up Selenium scenario..." );
my %SeleniumTestsConfig = %{ $Kernel::OM->Get('Kernel::Config')->Get('SeleniumTestsConfig') // {} };
if ( !%SeleniumTestsConfig ) {
my $Self = bless {}, $Class;
$Self->{UnitTestDriverObject} = $Param{UnitTestDriverObject};
return $Self;
}
for my $Needed (qw(remote_server_addr port browser_name platform)) {
if ( !$SeleniumTestsConfig{$Needed} ) {
die "SeleniumTestsConfig must provide $Needed!";
}
}
$Kernel::OM->Get('Kernel::System::Main')->RequireBaseClass('Selenium::Remote::Driver')
|| die "Could not load Selenium::Remote::Driver";
$Kernel::OM->Get('Kernel::System::Main')->Require('Kernel::System::UnitTest::Selenium::WebElement')
|| die "Could not load Kernel::System::UnitTest::Selenium::WebElement";
my $Self;
# TEMPORARY WORKAROUND FOR GECKODRIVER BUG https://github.com/mozilla/geckodriver/issues/1470:
# If marionette handshake fails, wait and try again. Can be removed after the bug is fixed
# in a new geckodriver version.
eval {
$Self = $Class->SUPER::new(
webelement_class => 'Kernel::System::UnitTest::Selenium::WebElement',
%SeleniumTestsConfig
);
};
if ($@) {
my $Exception = $@;
# Only handle this specific geckodriver exception.
die $Exception if $Exception !~ m{Socket timeout reading Marionette handshake data};
# Sleep and try again, bail out if it fails a second time.
# A long sleep of 10 seconds is acceptable here, as it occurs only very rarely.
sleep 10;
$Self = $Class->SUPER::new(
webelement_class => 'Kernel::System::UnitTest::Selenium::WebElement',
%SeleniumTestsConfig
);
}
$Self->{UnitTestDriverObject} = $Param{UnitTestDriverObject};
$Self->{SeleniumTestsActive} = 1;
$Self->{UnitTestDriverObject}->{SeleniumData} = { %{ $Self->get_capabilities() }, %{ $Self->status() } };
#$Self->debug_on();
# set screen size from config or use defauls
my $Height = $SeleniumTestsConfig{window_height} || 1200;
my $Width = $SeleniumTestsConfig{window_width} || 1400;
$Self->set_window_size( $Height, $Width );
$Self->{BaseURL} = $Kernel::OM->Get('Kernel::Config')->Get('HttpType') . '://';
$Self->{BaseURL} .= Kernel::System::UnitTest::Helper->GetTestHTTPHostname();
# Remember the start system time for the selenium test run.
$Self->{TestStartSystemTime} = time; ## no critic
return $Self;
}
=head2 RunTest()
runs a selenium test if Selenium testing is configured.
$SeleniumObject->RunTest( sub { ... } );
=cut
sub RunTest {
my ( $Self, $Test ) = @_;
if ( !$Self->{SeleniumTestsActive} ) {
$Self->{UnitTestDriverObject}->True( 1, 'Selenium testing is not active, skipping tests.' );
return 1;
}
eval {
$Test->();
};
$TestException = $@ if $@;
return 1;
}
=begin Internal:
=head2 _execute_command()
Override internal command of base class.
We use it to output successful command runs to the UnitTest object.
Errors will cause an exeption and be caught elsewhere.
=end Internal:
=cut
sub _execute_command { ## no critic
my ( $Self, $Res, $Params ) = @_;
my $Result = $Self->SUPER::_execute_command( $Res, $Params );
my $TestName = 'Selenium command success: ';
$TestName .= $Kernel::OM->Get('Kernel::System::Main')->Dump(
{
%{ $Res || {} }, ## no critic
%{ $Params || {} }, ## no critic
}
);
if ( $Self->{SuppressCommandRecording} ) {
print $TestName;
}
else {
$Self->{UnitTestDriverObject}->True( 1, $TestName );
}
return $Result;
}
=head2 get()
Override get method of base class to prepend the correct base URL.
$SeleniumObject->get(
$URL,
);
=cut
sub get { ## no critic
my ( $Self, $URL ) = @_;
if ( $URL !~ m{http[s]?://}smx ) {
$URL = "$Self->{BaseURL}/$URL";
}
$Self->SUPER::get($URL);
return;
}
=head2 get_alert_text()
Override get_alert_text() method of base class to return alert text as string.
my $AlertText = $SeleniumObject->get_alert_text();
returns
my $AlertText = 'Some alert text!'
=cut
sub get_alert_text { ## no critic
my ($Self) = @_;
my $AlertText = $Self->SUPER::get_alert_text();
die "Alert dialog is not present" if ref $AlertText eq 'HASH'; # Chrome returns HASH when there is no alert text.
return $AlertText;
}
=head2 VerifiedGet()
perform a get() call, but wait for the page to be fully loaded (works only within OTRS).
Will die() if the verification fails.
$SeleniumObject->VerifiedGet(
$URL,
);
=cut
sub VerifiedGet {
my ( $Self, $URL ) = @_;
$Self->get($URL);
$Self->WaitFor(
JavaScript =>
'return typeof(Core) == "object" && typeof(Core.App) == "object" && Core.App.PageLoadComplete'
) || die "OTRS API verification failed after page load.";
return;
}
=head2 VerifiedRefresh()
perform a refresh() call, but wait for the page to be fully loaded (works only within OTRS).
Will die() if the verification fails.
$SeleniumObject->VerifiedRefresh();
=cut
sub VerifiedRefresh {
my ( $Self, $URL ) = @_;
$Self->refresh();
$Self->WaitFor(
JavaScript =>
'return typeof(Core) == "object" && typeof(Core.App) == "object" && Core.App.PageLoadComplete'
) || die "OTRS API verification failed after page load.";
return;
}
=head2 Login()
login to agent or customer interface
$SeleniumObject->Login(
Type => 'Agent', # Agent|Customer
User => 'someuser',
Password => 'somepassword',
);
=cut
sub Login {
my ( $Self, %Param ) = @_;
# check needed stuff
for (qw(Type User Password)) {
if ( !$Param{$_} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $_!",
);
return;
}
}
$Self->{UnitTestDriverObject}->True( 1, 'Initiating login...' );
# we will try several times to log in
my $MaxTries = 5;
TRY:
for my $Try ( 1 .. $MaxTries ) {
eval {
my $ScriptAlias = $Kernel::OM->Get('Kernel::Config')->Get('ScriptAlias');
if ( $Param{Type} eq 'Agent' ) {
$ScriptAlias .= 'index.pl';
}
else {
$ScriptAlias .= 'customer.pl';
}
$Self->get("${ScriptAlias}");
$Self->delete_all_cookies();
$Self->VerifiedGet("${ScriptAlias}?Action=Login;User=$Param{User};Password=$Param{Password}");
# login successful?
$Self->find_element( 'a#LogoutButton', 'css' ); # dies if not found
$Self->{UnitTestDriverObject}->True( 1, 'Login sequence ended...' );
};
# an error happend
if ($@) {
$Self->{UnitTestDriverObject}->True( 1, "Login attempt $Try of $MaxTries not successful." );
# try again
next TRY if $Try < $MaxTries;
die "Login failed!";
}
# login was sucessful
else {
last TRY;
}
}
return 1;
}
=head2 WaitFor()
wait with increasing sleep intervals until the given condition is true or the wait time is over.
Exactly one condition (JavaScript or WindowCount) must be specified.
my $Success = $SeleniumObject->WaitFor(
AlertPresent => 1, # Wait until an alert, confirm or prompt dialog is present
Callback => sub { ... } # Wait until function returns true
ElementExists => 'xpath-selector' # Wait until an element is present
ElementExists => ['css-selector', 'css'],
ElementMissing => 'xpath-selector', # Wait until an element is not present
ElementMissing => ['css-selector', 'css'],
JavaScript => 'return $(".someclass").length', # Javascript code that checks condition
WindowCount => 2, # Wait until this many windows are open
Time => 20, # optional, wait time in seconds (default 20)
);
=cut
sub WaitFor {
my ( $Self, %Param ) = @_;
if (
!$Param{JavaScript}
&& !$Param{WindowCount}
&& !$Param{AlertPresent}
&& !$Param{Callback}
&& !$Param{ElementExists}
&& !$Param{ElementMissing}
)
{
die "Need JavaScript, WindowCount, ElementExists, ElementMissing or AlertPresent.";
}
local $Self->{SuppressCommandRecording} = 1;
$Param{Time} //= 20;
my $WaitedSeconds = 0;
my $Interval = 0.1;
my $WaitSeconds = 0.5;
while ( $WaitedSeconds <= $Param{Time} ) {
if ( $Param{JavaScript} ) {
return 1 if $Self->execute_script( $Param{JavaScript} );
}
elsif ( $Param{WindowCount} ) {
return 1 if scalar( @{ $Self->get_window_handles() } ) == $Param{WindowCount};
}
elsif ( $Param{AlertPresent} ) {
# Eval is needed because the method would throw if no alert is present (yet).
return 1 if eval { $Self->get_alert_text() };
}
elsif ( $Param{Callback} ) {
return 1 if $Param{Callback}->();
}
elsif ( $Param{ElementExists} ) {
my @Arguments
= ref( $Param{ElementExists} ) eq 'ARRAY' ? @{ $Param{ElementExists} } : $Param{ElementExists};
if ( eval { $Self->find_element(@Arguments) } ) {
Time::HiRes::sleep($WaitSeconds);
return 1;
}
}
elsif ( $Param{ElementMissing} ) {
my @Arguments
= ref( $Param{ElementMissing} ) eq 'ARRAY' ? @{ $Param{ElementMissing} } : $Param{ElementMissing};
if ( !eval { $Self->find_element(@Arguments) } ) {
Time::HiRes::sleep($WaitSeconds);
return 1;
}
}
Time::HiRes::sleep($Interval);
$WaitedSeconds += $Interval;
$Interval += 0.1;
}
my $Argument = '';
for my $Key (qw(JavaScript WindowCount AlertPresent)) {
$Argument = "$Key => $Param{$Key}" if $Param{$Key};
}
$Argument = "Callback" if $Param{Callback};
die "WaitFor($Argument) failed.";
}
=head2 SwitchToFrame()
Change focus to another frame on the page. If C<WaitForLoad> is passed, it will wait until the frame has loaded the
page completely.
my $Success = $SeleniumObject->SwitchToFrame(
FrameSelector => '.Iframe', # (required) CSS selector of the frame element
WaitForLoad => 1, # (optional) Wait until the frame has loaded, if necessary
Time => 20, # (optional) Wait time in seconds (default 20)
);
=cut
sub SwitchToFrame {
my ( $Self, %Param ) = @_;
if ( !$Param{FrameSelector} ) {
die 'Need FrameSelector.';
}
if ( $Param{WaitForLoad} ) {
$Self->WaitFor(
JavaScript => "return typeof(\$('$Param{FrameSelector}').get(0).contentWindow.Core) == 'object'
&& typeof(\$('$Param{FrameSelector}').get(0).contentWindow.Core.App) == 'object'
&& \$('$Param{FrameSelector}').get(0).contentWindow.Core.App.PageLoadComplete;",
Time => $Param{Time},
);
}
$Self->switch_to_frame( $Self->find_element( $Param{FrameSelector}, 'css' ) );
return 1;
}
=head2 DragAndDrop()
Drag and drop an element.
$SeleniumObject->DragAndDrop(
Element => '.Element', # (required) css selector of element which should be dragged
Target => '.Target', # (required) css selector of element on which the dragged element should be dropped
TargetOffset => { # (optional) Offset for target. If not specified, the mouse will move to the middle of the element.
X => 150,
Y => 100,
}
);
=cut
sub DragAndDrop {
my ( $Self, %Param ) = @_;
# Value is optional parameter
for my $Needed (qw(Element Target)) {
if ( !$Param{$Needed} ) {
die "Need $Needed";
}
}
my %TargetOffset;
if ( $Param{TargetOffset} ) {
%TargetOffset = (
xoffset => $Param{TargetOffset}->{X} || 0,
yoffset => $Param{TargetOffset}->{Y} || 0,
);
}
# Make sure Element is visible
$Self->WaitFor(
JavaScript => 'return typeof($) === "function" && $(\'' . $Param{Element} . ':visible\').length;',
);
my $Element = $Self->find_element( $Param{Element}, 'css' );
# Move mouse to from element, drag and drop
$Self->mouse_move_to_location( element => $Element );
# Holds the mouse button on the element
$Self->button_down();
# Make sure Target is visible
$Self->WaitFor(
JavaScript => 'return typeof($) === "function" && $(\'' . $Param{Target} . ':visible\').length;',
);
my $Target = $Self->find_element( $Param{Target}, 'css' );
# Move mouse to the destination
$Self->mouse_move_to_location(
element => $Target,
%TargetOffset,
);
# Release
$Self->button_up();
return;
}
=head2 HandleError()
use this method to handle any Selenium exceptions.
$SeleniumObject->HandleError($@);
It will create a failing test result and store a screen shot of the page
for analysis (in folder /var/otrs-unittest if it exists, in $Home/var/httpd/htdocs otherwise).
=cut
sub HandleError {
my ( $Self, $Error ) = @_;
$Self->{UnitTestDriverObject}->False( 1, "Exception in Selenium': $Error" );
#eval {
my $Data = $Self->screenshot();
return if !$Data;
$Data = MIME::Base64::decode_base64($Data);
#
# Store screenshots in a local folder from where they can be opened directly in the browser.
#
my $LocalScreenshotDir = $Kernel::OM->Get('Kernel::Config')->Get('Home') . '/var/httpd/htdocs/SeleniumScreenshots';
mkdir $LocalScreenshotDir || return $Self->False( 1, "Could not create $LocalScreenshotDir." );
my $DateTimeObj = $Kernel::OM->Create('Kernel::System::DateTime');
my $Filename = $DateTimeObj->ToString();
$Filename .= '-' . ( int rand 100_000_000 ) . '.png';
$Filename =~ s{[ :]}{-}smxg;
my $HttpType = $Kernel::OM->Get('Kernel::Config')->Get('HttpType');
my $Hostname = $Kernel::OM->Get('Kernel::System::UnitTest::Helper')->GetTestHTTPHostname();
my $URL = "$HttpType://$Hostname/"
. $Kernel::OM->Get('Kernel::Config')->Get('Frontend::WebPath')
. "SeleniumScreenshots/$Filename";
$Kernel::OM->Get('Kernel::System::Main')->FileWrite(
Directory => $LocalScreenshotDir,
Filename => $Filename,
Content => \$Data,
) || return $Self->False( 1, "Could not write file $LocalScreenshotDir/$Filename" );
#
# If a shared screenshot folder is present, then we also store the screenshot there for external use.
#
if ( -d '/var/otrs-unittest/' && -w '/var/otrs-unittest/' ) {
my $SharedScreenshotDir = '/var/otrs-unittest/SeleniumScreenshots';
mkdir $SharedScreenshotDir || return $Self->False( 1, "Could not create $SharedScreenshotDir." );
$Kernel::OM->Get('Kernel::System::Main')->FileWrite(
Directory => $SharedScreenshotDir,
Filename => $Filename,
Content => \$Data,
)
|| return $Self->{UnitTestDriverObject}->False( 1, "Could not write file $SharedScreenshotDir/$Filename" );
}
$Self->{UnitTestDriverObject}->False( 1, "Saved screenshot in $URL" );
$Self->{UnitTestDriverObject}->AttachSeleniumScreenshot(
Filename => $Filename,
Content => $Data
);
return;
}
=head2 DEMOLISH()
override DEMOLISH from L<Selenium::Remote::Driver> (required because this class is managed by L<Moo>).
Performs proper error handling (calls C<HandleError()> if needed). Adds a unit test result to indicate the shutdown,
and performs some clean-ups.
=cut
sub DEMOLISH {
my $Self = shift;
if ($TestException) {
$Self->HandleError($TestException);
}
# Could be missing on early die.
if ( $Self->{UnitTestDriverObject} ) {
$Self->{UnitTestDriverObject}->True( 1, "Shutting down Selenium scenario." );
}
if ( $Self->{SeleniumTestsActive} ) {
$Self->SUPER::DEMOLISH(@_);
# Cleanup possibly leftover zombie firefox profiles.
my @LeftoverFirefoxProfiles = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead(
Directory => '/tmp/',
Filter => 'anonymous*webdriver-profile',
);
for my $LeftoverFirefoxProfile (@LeftoverFirefoxProfiles) {
if ( -d $LeftoverFirefoxProfile ) {
File::Path::remove_tree($LeftoverFirefoxProfile);
}
}
# Cleanup all sessions, which was created after the selenium test start time.
my $AuthSessionObject = $Kernel::OM->Get('Kernel::System::AuthSession');
my @Sessions = $AuthSessionObject->GetAllSessionIDs();
SESSION:
for my $SessionID (@Sessions) {
my %SessionData = $AuthSessionObject->GetSessionIDData( SessionID => $SessionID );
next SESSION if !%SessionData;
next SESSION
if $SessionData{UserSessionStart} && $SessionData{UserSessionStart} < $Self->{TestStartSystemTime};
$AuthSessionObject->RemoveSessionID( SessionID => $SessionID );
}
}
return;
}
=head1 DEPRECATED FUNCTIONS
=head2 WaitForjQueryEventBound()
waits until event handler is bound to the selected C<jQuery> element. Deprecated - it will be removed in the future releases.
$SeleniumObject->WaitForjQueryEventBound(
CSSSelector => 'li > a#Test', # (required) css selector
Event => 'click', # (optional) Specify event name. Default 'click'.
);
=cut
sub WaitForjQueryEventBound {
my ( $Self, %Param ) = @_;
# Check needed stuff.
if ( !$Param{CSSSelector} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need CSSSelector!",
);
return;
}
my $Event = $Param{Event} || 'click';
# Wait for element availability.
$Self->WaitFor(
JavaScript => 'return typeof($) === "function" && $("' . $Param{CSSSelector} . '").length;'
);
# Wait for jQuery initialization.
$Self->WaitFor(
JavaScript =>
'return Object.keys($("' . $Param{CSSSelector} . '")[0]).length > 0'
);
# Get jQuery object keys.
my $Keys = $Self->execute_script(
'return Object.keys($("' . $Param{CSSSelector} . '")[0]);'
);
if ( !IsArrayRefWithData($Keys) ) {
die "Couldn't determine jQuery object id";
}
my $JQueryObjectID;
KEY:
for my $Key ( @{$Keys} ) {
if ( $Key =~ m{^jQuery\d+$} ) {
$JQueryObjectID = $Key;
last KEY;
}
}
if ( !$JQueryObjectID ) {
die "Couldn't determine jQuery object id.";
}
# Wait until click event is bound to the element.
$Self->WaitFor(
JavaScript =>
'return $("' . $Param{CSSSelector} . '")[0].' . $JQueryObjectID . '.events
&& $("' . $Param{CSSSelector} . '")[0].' . $JQueryObjectID . '.events.' . $Event . '
&& $("' . $Param{CSSSelector} . '")[0].' . $JQueryObjectID . '.events.' . $Event . '.length > 0;',
);
return 1;
}
=head2 InputFieldValueSet()
sets modernized input field value.
$SeleniumObject->InputFieldValueSet(
Element => 'css-selector', # (required) css selector
Value => 3, # (optional) Value
);
=cut
sub InputFieldValueSet {
my ( $Self, %Param ) = @_;
# Check needed stuff.
if ( !$Param{Element} ) {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need Element!",
);
die 'Missing Element.';
}
my $Value = $Param{Value} // '';
if ( $Value !~ m{^\[} && $Value !~ m{^".*"$} ) {
# Quote text of Value is not array and if not already quoted.
$Value = "\"$Value\"";
}
# Set selected value.
$Self->execute_script(
"\$('$Param{Element}').val($Value).trigger('redraw.InputField').trigger('change');"
);
# Wait until selection tree is closed.
$Self->WaitFor(
ElementMissing => [ '.InputField_ListContainer', 'css' ],
);
return 1;
}
1;
=head1 TERMS AND CONDITIONS
This software is part of the OTRS project (L<https://otrs.org/>).
This software comes with ABSOLUTELY NO WARRANTY. For details, see
the enclosed file COPYING for license information (GPL). If you
did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
=cut

View File

@@ -0,0 +1,78 @@
# --
# Copyright (C) 2001-2019 OTRS AG, https://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (GPL). If you
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
# --
package Kernel::System::UnitTest::Selenium::WebElement;
use strict;
use warnings;
use parent qw(Selenium::Remote::WebElement);
=head1 NAME
Kernel::System::UnitTest::Selenium::WebElement - Utility functions for Selenium WebElements
=head2 VerifiedSubmit()
Submit a form element, and wait for the page to be fully loaded (works only in OTRS)
$SeleniumObject->VerifiedSubmit();
=cut
sub VerifiedSubmit {
my ( $Self, $Params ) = @_;
$Self->submit();
$Self->driver()->WaitFor(
JavaScript =>
'return typeof(Core) == "object" && typeof(Core.App) == "object" && Core.App.PageLoadComplete'
) || die "OTRS API verification failed after element submit.";
return;
}
=head2 VerifiedClick()
click an element that causes a page get/reload/submit and wait for the page to be fully loaded
(works only in OTRS).
$SeleniumObject->VerifiedClick(
$Button # optional, see Selenium docs
);
=cut
sub VerifiedClick { ## no critic
my $Self = shift;
$Self->driver()->execute_script('window.Core.App.PageLoadComplete = false;');
$Self->SUPER::click(@_);
$Self->driver()->WaitFor(
JavaScript =>
'return typeof(Core) == "object" && typeof(Core.App) == "object" && Core.App.PageLoadComplete'
) || die "OTRS API verification failed after element click.";
return;
}
1;
=head1 TERMS AND CONDITIONS
This software is part of the OTRS project (L<https://otrs.org/>).
This software comes with ABSOLUTELY NO WARRANTY. For details, see
the enclosed file COPYING for license information (GPL). If you
did not receive this file, see L<https://www.gnu.org/licenses/gpl-3.0.txt>.
=cut