Skip to content

Commit

Permalink
Add cross-request RW demos Behat testing (#2031)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvorisek committed Apr 29, 2023
1 parent 1408608 commit f45337e
Show file tree
Hide file tree
Showing 7 changed files with 250 additions and 6 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ cache
*.cache.*

/demos/db.php
/demos/db-behat-rw.txt
/demos/_demo-data/db.sqlite
/demos/_demo-data/db.sqlite-journal
/phpunit.xml
Expand Down
20 changes: 18 additions & 2 deletions demos/init-db.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@

trait ModelPreventModificationTrait
{
protected function isAllowDbModifications(): bool
{
static $rw = null;
if ($rw === null) {
$rw = file_exists(__DIR__ . '/db-behat-rw.txt');
}

return $rw;
}

public function atomic(\Closure $fx)
{
$eRollback = new \Exception('Prevent modification');
Expand All @@ -34,7 +44,9 @@ public function atomic(\Closure $fx)
parent::atomic(function () use ($fx, $eRollback, &$res) {
$res = $fx();

throw $eRollback;
if (!$this->isAllowDbModifications()) {
throw $eRollback;
}
});
} catch (\Exception $e) {
if ($e !== $eRollback) {
Expand All @@ -60,7 +72,11 @@ protected function wrapUserActionCallbackPreventModification(Model\UserAction $a
$callbackBackup = $action->callback;
try {
$action->callback = $originalCallback;
$action->execute(...$args);
$res = $action->execute(...$args);

if ($this->isAllowDbModifications()) {
return $res;
}
} finally {
$action->callback = $callbackBackup;
}
Expand Down
5 changes: 5 additions & 0 deletions src/Behat/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
class Context extends RawMinkContext implements BehatContext
{
use JsCoverageContextTrait;
use RwDemosContextTrait;
use WarnDynamicPropertyTrait;

public function getSession($name = null): MinkSession
Expand Down Expand Up @@ -71,6 +72,10 @@ public function closeAllToasts(BeforeStepScope $event): void
*/
public function waitUntilLoadingAndAnimationFinished(AfterStepScope $event): void
{
if (!$this->getSession()->getDriver()->isStarted()) {
return;
}

$this->jqueryWait();
$this->disableAnimations();

Expand Down
201 changes: 201 additions & 0 deletions src/Behat/RwDemosContextTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<?php

declare(strict_types=1);

namespace Atk4\Ui\Behat;

use Atk4\Data\Model;
use Atk4\Data\Persistence;

trait RwDemosContextTrait
{
protected string $demosDir = __DIR__ . '/../../demos';

protected bool $needDatabaseRestore = false;

/** @var list<string> */
protected array $databaseBackupTables = [
'client',
'country',
'file',
'stat',
'product_category',
'product_sub_category',
'product',
'multiline_item',
'multiline_delivery',
];

/** @var array<string, Model>|null */
protected ?array $databaseBackupModels = null;

/** @var array<string, array<int, array<string, mixed>>>|null */
protected ?array $databaseBackupData = null;

protected function getDemosDb(): Persistence\Sql
{
static $db = null;
if ($db === null) {
try {
/** @var Persistence\Sql $db */
require_once $this->demosDir . '/init-db.php'; // @phpstan-ignore-line
} catch (\Throwable $e) {
throw new \Exception('Database error: ' . $e->getMessage());
}
}

return $db;
}

protected function createDatabaseModelFromTable(string $table): Model
{
$db = $this->getDemosDb();
$schemaManager = $db->getConnection()->createSchemaManager();
$tableColumns = $schemaManager->listTableColumns($table);

$model = new Model($db, ['table' => $table]);
$model->removeField('id');
foreach ($tableColumns as $tableColumn) {
$model->addField($tableColumn->getName(), [
'type' => $tableColumn->getType()->getName(), // @phpstan-ignore-line Type::getName() is deprecated in DBAL 4.0
'nullable' => !$tableColumn->getNotnull(),
]);
}
$model->idField = array_key_first($model->getFields());

return $model;
}

protected function createDatabaseModels(): void
{
$modelByTable = [];
foreach ($this->databaseBackupTables as $table) {
$modelByTable[$table] = $this->createDatabaseModelFromTable($table);
}

$this->databaseBackupModels = $modelByTable;
}

protected function createDatabaseBackup(): void
{
$dataByTable = [];
foreach ($this->databaseBackupTables as $table) {
$model = $this->databaseBackupModels[$table];

$data = [];
foreach ($model as $entity) {
$data[$entity->getId()] = $entity->get();
}

$dataByTable[$table] = $data;
}

$this->databaseBackupData = $dataByTable;
}

/**
* @return array<string, \stdClass&object{ addedIds: list<int>, changedIds: list<int>, deletedIds: list<int> }>
*/
protected function discoverDatabaseChanges(): array
{
$changesByTable = [];
foreach ($this->databaseBackupTables as $table) {
$model = $this->databaseBackupModels[$table];
$data = $this->databaseBackupData[$table];

$changes = new \stdClass();
$changes->addedIds = [];
$changes->changedIds = [];
$changes->deletedIds = array_fill_keys(array_keys($data), true);
foreach ($model as $entity) {
$id = $entity->getId();
if (!isset($data[$id])) {
$changes->addedIds[] = $id;
} else {
$isChanged = false;
foreach ($data[$id] as $k => $v) {
if (!$entity->compare($k, $v)) {
$isChanged = true;

break;
}
}

if ($isChanged) {
$changes->changedIds[] = $id;
}

unset($changes->deletedIds[$id]);
}
}
$changes->deletedIds = array_keys($changes->deletedIds);

if (count($changes->addedIds) > 0 || count($changes->changedIds) > 0 || count($changes->deletedIds) > 0) {
$changesByTable[$table] = $changes;
}
}

return $changesByTable; // @phpstan-ignore-line https://github.com/phpstan/phpstan/issues/9252
}

protected function restoreDatabaseBackup(): void
{
$changesByTable = $this->discoverDatabaseChanges();

if (count($changesByTable) > 0) {
// TODO disable FK checks
// unfortunately there is no DBAL API - https://github.com/doctrine/dbal/pull/2620
try {
$this->getDemosDb()->atomic(function () use ($changesByTable) {
foreach ($changesByTable as $table => $changes) {
$model = $this->databaseBackupModels[$table];
$data = $this->databaseBackupData[$table];

foreach ($changes->addedIds as $id) {
$model->delete($id);
}

foreach ([...$changes->changedIds, ...$changes->deletedIds] as $id) {
$entity = in_array($id, $changes->changedIds, true) ? $model->load($id) : $model->createEntity();
$entity->setMulti($data[$id]);
$entity->save();
}
}
});
} finally {
// TODO enable FK checks
}
}
}

/**
* @AfterScenario
*/
public function restoreDatabase(): void
{
if ($this->needDatabaseRestore) {
$this->needDatabaseRestore = false;
unlink($this->demosDir . '/db-behat-rw.txt');

$this->restoreDatabaseBackup();
}
}

/**
* @When I persist DB changes across requests
*/
public function iPersistDbChangesAcrossRequests(): void
{
if ($this->databaseBackupData === null) {
if (file_exists($this->demosDir . '/db-behat-rw.txt')) {
throw new \Exception('Database was not restored cleanly');
}

$this->createDatabaseModels();
$this->createDatabaseBackup();
}

$this->needDatabaseRestore = true;
file_put_contents($this->demosDir . '/db-behat-rw.txt', '');
}
}
3 changes: 2 additions & 1 deletion src/CardSection.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ private function addSectionFields(Model $model, array $fields, bool $useLabel =
if ($model->titleField === $field) {
continue;
}
$label = $model->getField($field)->getCaption();

$value = $this->getApp()->uiPersistence->typecastSaveField($model->getField($field), $model->get($field));
if ($useLabel) {
$label = $model->getField($field)->getCaption();
$value = $label . $this->glue . $value;
}

Expand Down
10 changes: 7 additions & 3 deletions tests-behat/card-deck.feature
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ Feature: CardDeck
Then I press button "Delete"
Then I press Modal button "Ok"
Then Toast display should contain text 'Country action "delete" with "United Kingdom" entity was executed.'
# TODO CardDeck reload is fired in separate AJAX request, thus the changes
# cannot be tested with Behat, as reverted in the first request
# Then I should not see "United Kingdom"

Scenario: delete - with unlocked DB
When I persist DB changes across requests
Then I press button "Delete"
Then I press Modal button "Ok"
Then Toast display should contain text 'Record has been deleted!'
Then I should not see "United Kingdom"
16 changes: 16 additions & 0 deletions tests-behat/crud.feature
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ Feature: Crud
# make sure search query stick
Then I should see "United Kingdom"

Scenario: edit - with unlocked DB
# hotfix "element not interactable"
# TODO modal should be always fully (re)loaded on open and fully destroyed once it is closed
# https://github.com/atk4/ui/issues/1928
Given I am on "_unit-test/crud.php"
Then I search grid for "united kingdom"

Then I should not see "My United Kingdom"
When I persist DB changes across requests
Then I press button "Edit"
Then Modal is open with text "Edit Country"
Then I fill in "atk_fp_country__name" with "My United Kingdom"
Then I press Modal button "Save"
Then Toast display should contain text 'Record has been saved!'
Then I should see "My United Kingdom"

Scenario: delete
Then I press button "Delete"
Then I press Modal button "Ok"
Expand Down

0 comments on commit f45337e

Please sign in to comment.