Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/Pages/v9/BackOffice/Login/Page.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,41 @@ public function login($email = null, $password = null, $waitForNavigation = true
if ($waitForNavigation) {
$this->click($this->getSelector('submitLoginButton'));
$this->waitForPageReload();
$this->waitForLoginOutcome();
} else {
$this->click($this->getSelector('submitLoginButton'));
}
}

/**
* Wait until the login has an outcome: the logout link (session open) or
* an error alert with text in it (credentials refused).
*
* waitForPageReload() alone is a fixed 10 s, and isLoggedIn() then looks
* for 5 s more. The first dashboard after a cache clear can take longer
* than that on a loaded CI runner: the session was open, the dashboard was
* still loading, and the login was reported as refused (PS 9.2, reproduced
* with a CPU-throttled shop). Waiting on the outcome costs nothing when
* the shop is fast — either signal ends the wait as soon as it shows — and
* the ceiling is only reached by a shop that is actually broken.
*
* The alert must have TEXT: 1.7 and 8 ship an empty .alert-danger in the
* login page (see getLoginError()). Returns whether an outcome was seen;
* callers still assert with isLoggedIn() / getLoginError().
*/
public function waitForLoginOutcome(int $timeout = 60000, int $interval = 200): bool
{
return $this->waitForJsCondition(
sprintf(
'!!document.querySelector(%s) || (function(){var e=document.querySelector(%s);return !!e && e.textContent.trim() !== "";})()',
json_encode($this->getSelector('logoutLink')),
json_encode($this->getSelector('alertDangerTextBlock'))
),
$timeout,
$interval
);
}

/**
* Whether an employee session is actually open.
*
Expand Down
18 changes: 15 additions & 3 deletions src/Tests/VisualTestsSuite.php
Original file line number Diff line number Diff line change
Expand Up @@ -418,9 +418,10 @@ protected function ensureBackOfficeLogin(?object $login): void
}
$login->login(); // identifiants des globals BO_EMAIL / BO_PASSWD
if (!$login->isLoggedIn()) {
$error = $this->readNow($login, sprintf('(function(){var e=document.querySelector(%s);return e?e.textContent:"";})()', json_encode((string) $login->getSelector('alertDangerDiv'))));
$error = trim(is_string($error) ? $error : '');
throw new \RuntimeException('identifiants refusés ou page inattendue'.($error !== '' ? ' : '.$error : ''));
// 9.2 : .alert-text, sans le bouton de fermeture (« close ») ; 1.7 et 8 : l'alerte entière.
$error = $this->readNow($login, sprintf('(function(){var e=document.querySelector(%s);if(!e)return "";return (e.querySelector(".alert-text")||e).textContent;})()', json_encode((string) $login->getSelector('alertDangerDiv'))));
$error = trim(preg_replace('/\s+/', ' ', is_string($error) ? $error : ''));
throw new \RuntimeException('identifiants refusés ou page inattendue'.($error !== '' ? ' : '.$error : $this->whereIs($login)));
}
$this->boLoggedIn = true;

Expand All @@ -433,6 +434,17 @@ protected function ensureBackOfficeLogin(?object $login): void
throw new \RuntimeException('Connexion au back-office impossible : '.$this->boLoginError);
}

/**
* Sans alerte, la connexion n'a pas été refusée : l'onglet est ailleurs. On
* donne le chemin et le contrôleur, jamais le jeton de l'URL.
*/
protected function whereIs(object $page): string
{
$where = $this->readNow($page, '(function(){var c=new URLSearchParams(location.search).get("controller");return location.pathname+(c?"?controller="+c:"");})()');

return is_string($where) && $where !== '' ? ' (page : '.$where.')' : '';
}

/** Formulaire de connexion affiché sur la page courante (lu sans attendre, sélecteur de la page Login). */
protected function loginFormPresent(object $page, ?object $login): bool
{
Expand Down
146 changes: 146 additions & 0 deletions tests/Unit/Pages/BackOfficeLoginOutcomeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

namespace PrestaFlow\Tests\Unit\Pages;

use PHPUnit\Framework\TestCase;
use PrestaFlow\Library\Pages\v9\BackOffice\Login\Page as LoginPage;

final class FakeLoginOutcomeEvaluation
{
public function __construct(private mixed $value)
{
}

public function getReturnValue(?int $timeout = null): mixed
{
return $this->value;
}
}

final class FakeLoginOutcomeBrowserPage
{
public function __construct(private FakeLoginOutcomePage $owner)
{
}

public function evaluate(string $js): FakeLoginOutcomeEvaluation
{
$this->owner->polls++;
$this->owner->evaluated[] = $js;

return new FakeLoginOutcomeEvaluation($this->owner->settled());
}
}

/**
* Test double for a login whose next page is slow: the outcome (session open,
* or error shown) only becomes visible after $settlesOnPoll polls. A cold
* dashboard on a loaded CI runner took longer than the old fixed 10 s + 5 s.
*/
final class FakeLoginOutcomePage extends LoginPage
{
public int $polls = 0;
public int $settlesOnPoll = 3;
public bool $neverSettles = false;
public array $evaluated = [];
public array $log = [];

public function settled(): bool
{
return !$this->neverSettles && $this->polls >= $this->settlesOnPoll;
}

public function getPage()
{
return new FakeLoginOutcomeBrowserPage($this);
}

public function setValue($selector, $value)
{
$this->log[] = 'set '.$selector;
}

public function click($selector, $nth = 1)
{
$this->log[] = 'click '.$selector;
}

public function waitForPageReload()
{
$this->log[] = 'reload';
}
}

final class BackOfficeLoginOutcomeTest extends TestCase
{
private function page(): FakeLoginOutcomePage
{
return new FakeLoginOutcomePage(
locale: 'en',
patchVersion: '9.2.0',
globals: [
'PS_VERSION' => '9.2.0',
'LOCALE' => 'en',
'PREFIX_LOCALE' => false,
'BO' => ['URL' => 'http://localhost/admin-dev/', 'EMAIL' => 'a@b.c', 'PASSWD' => 'x'],
'FO' => ['URL' => 'http://localhost/', 'EMAIL' => 'a@b.c', 'PASSWD' => 'x'],
'DEBUG' => false,
'VERBOSE' => false,
],
customs: []
);
}

public function testASlowOutcomeIsWaitedFor(): void
{
$page = $this->page();
$page->settlesOnPoll = 4;

$this->assertTrue($page->waitForLoginOutcome(2000, 1));
$this->assertSame(4, $page->polls);
}

public function testTheOutcomeIsTheLogoutLinkOrAFilledErrorAlert(): void
{
$page = $this->page();
$page->settlesOnPoll = 1;

$page->waitForLoginOutcome(2000, 1);

$js = $page->evaluated[0];
$this->assertStringContainsString('#header_logout', $js);
$this->assertStringContainsString('.alert-danger', $js);
// An empty alert container (1.7 / 8 ship one) is not an outcome.
$this->assertStringContainsString('textContent', $js);
}

public function testAnOutcomeThatNeverComesReturnsFalseRatherThanThrowing(): void
{
$page = $this->page();
$page->neverSettles = true;

$this->assertFalse($page->waitForLoginOutcome(150, 10));
$this->assertGreaterThan(1, $page->polls);
}

public function testLoginWaitsForTheOutcomeAfterTheReload(): void
{
$page = $this->page();
$page->settlesOnPoll = 2;

$page->login();

$this->assertSame(['set #email', 'set #passwd', 'click #submit_login', 'reload'], $page->log);
$this->assertSame(2, $page->polls, 'login() must poll until the outcome is visible');
}

public function testLoginWithoutNavigationWaitDoesNotWait(): void
{
$page = $this->page();

$page->login(waitForNavigation: false);

$this->assertSame(['set #email', 'set #passwd', 'click #submit_login'], $page->log);
$this->assertSame(0, $page->polls);
}
}
32 changes: 32 additions & 0 deletions tests/Unit/Visual/VisualTestsSuiteBackOfficeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@ protected function setUp(): void
$this->chrome = new class {
public bool $form = true;
public string $error = '';
public string $location = '';
public function evaluate(string $js): object
{
$value = match (true) {
str_contains($js, '#email') => $this->form,
str_contains($js, '.alert-danger') => $this->error,
str_contains($js, 'location.pathname') => $this->location,
default => null,
};

Expand Down Expand Up @@ -351,6 +353,36 @@ public function test_refused_login_reports_the_form_error(): void
);
}

public function test_refused_login_reports_the_form_error_on_one_line(): void
{
// 9.2 : bouton de fermeture puis .alert-text, sur plusieurs lignes ; seule
// la première ligne (« close ») apparaissait dans le rapport.
$this->chrome->error = "\n The employee does not exist,\n or the password provided is incorrect.\n";
$log = new \ArrayObject();
$s = $this->suite(page: $this->page($log), login: $this->login($log, ok: false), checkpoints: [['name' => 'dashboard']]);
$s->init();

$this->assertSame(
['capture visuelle : dashboard' => 'Connexion au back-office impossible : identifiants refusés ou page inattendue : The employee does not exist, or the password provided is incorrect.'],
$this->runSteps($s)
);
}

public function test_unexpected_page_reports_where_the_browser_is(): void
{
// Pas d'alerte : la connexion n'a pas été refusée, l'onglet est ailleurs.
// Le chemin et le contrôleur suffisent au diagnostic, pas le jeton.
$this->chrome->location = '/admin-dev/?controller=AdminDashboard';
$log = new \ArrayObject();
$s = $this->suite(page: $this->page($log), login: $this->login($log, ok: false), checkpoints: [['name' => 'dashboard']]);
$s->init();

$this->assertSame(
['capture visuelle : dashboard' => 'Connexion au back-office impossible : identifiants refusés ou page inattendue (page : /admin-dev/?controller=AdminDashboard)'],
$this->runSteps($s)
);
}

public function test_failed_checkpoint_forces_navigation_on_next_one_with_same_target(): void
{
$log = new \ArrayObject();
Expand Down
Loading