From f45337e82bd86511d625ddf86342455bd3c49015 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Sat, 29 Apr 2023 11:05:23 +0200 Subject: [PATCH] Add cross-request RW demos Behat testing (#2031) --- .gitignore | 1 + demos/init-db.php | 20 ++- src/Behat/Context.php | 5 + src/Behat/RwDemosContextTrait.php | 201 ++++++++++++++++++++++++++++++ src/CardSection.php | 3 +- tests-behat/card-deck.feature | 10 +- tests-behat/crud.feature | 16 +++ 7 files changed, 250 insertions(+), 6 deletions(-) create mode 100644 src/Behat/RwDemosContextTrait.php diff --git a/.gitignore b/.gitignore index f91a9d78d2..6dfde935bf 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/demos/init-db.php b/demos/init-db.php index 98b2dd0be2..80cb1e2745 100644 --- a/demos/init-db.php +++ b/demos/init-db.php @@ -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'); @@ -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) { @@ -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; } diff --git a/src/Behat/Context.php b/src/Behat/Context.php index 8819d4c4db..106477a8ea 100644 --- a/src/Behat/Context.php +++ b/src/Behat/Context.php @@ -17,6 +17,7 @@ class Context extends RawMinkContext implements BehatContext { use JsCoverageContextTrait; + use RwDemosContextTrait; use WarnDynamicPropertyTrait; public function getSession($name = null): MinkSession @@ -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(); diff --git a/src/Behat/RwDemosContextTrait.php b/src/Behat/RwDemosContextTrait.php new file mode 100644 index 0000000000..e9a782092d --- /dev/null +++ b/src/Behat/RwDemosContextTrait.php @@ -0,0 +1,201 @@ + */ + protected array $databaseBackupTables = [ + 'client', + 'country', + 'file', + 'stat', + 'product_category', + 'product_sub_category', + 'product', + 'multiline_item', + 'multiline_delivery', + ]; + + /** @var array|null */ + protected ?array $databaseBackupModels = null; + + /** @var array>>|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, changedIds: list, deletedIds: list }> + */ + 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', ''); + } +} diff --git a/src/CardSection.php b/src/CardSection.php index 083667b815..3da9e752ca 100644 --- a/src/CardSection.php +++ b/src/CardSection.php @@ -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; } diff --git a/tests-behat/card-deck.feature b/tests-behat/card-deck.feature index 329a47c15f..463a06859d 100644 --- a/tests-behat/card-deck.feature +++ b/tests-behat/card-deck.feature @@ -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" diff --git a/tests-behat/crud.feature b/tests-behat/crud.feature index f7dea97e15..d9031a6bd0 100644 --- a/tests-behat/crud.feature +++ b/tests-behat/crud.feature @@ -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"