path = tempnam(sys_get_temp_dir(), 'kant-form-'); $this->db = ConnectionFactory::create('sqlite:///'.str_replace('\\', '/', $this->path)); TestSchema::build($this->db); $this->db->insert('website', ['id' => 'default', 'name' => 'KantSites', 'default_locale' => 'de', 'theme' => '{}']); $this->db->insert('page', ['id' => 'page-1', 'slug' => 'home', 'path' => 'home', 'type' => 'page', 'locale' => 'de', 'website_id' => 'default', 'version' => 0, 'created_at' => gmdate('Y-m-d H:i:s')]); $settings = new SiteSettings($this->db, new MediaLibrary($this->db, $this->path.'-media')); $this->pages = new PageRepository($this->db, new DocumentValidator(), $settings); $this->forms = new FormRepository($this->db); $partials = new PartialRepository($this->db, new DocumentValidator()); foreach (DemoContent::areas() as $kind => $area) { $partials->create($kind, 'default', $area['name'], $area['document']); } } protected function tearDown(): void { $this->db->close(); gc_collect_cycles(); @unlink($this->path); } /** Ein kleiner Preisfinder: Objektart, davon abhängige Eckdaten, ein bedingter Schritt, Kontakt. */ private function preisfinder(): array { return [ ['name' => 'schritt_objekt', 'type' => 'step', 'label' => 'Ihre Immobilie', 'help' => 'Womit können wir helfen?'], ['name' => 'immobilienart', 'type' => 'radio', 'label' => 'Objektart', 'required' => true, 'options' => ['Wohnung', 'Haus', 'Grundstück']], ['name' => 'schritt_eckdaten', 'type' => 'step', 'label' => 'Eckdaten'], ['name' => 'wohnflaeche', 'type' => 'number', 'label' => 'Wohnfläche', 'required' => true, 'min' => 10, 'max' => 2000, 'unit' => 'm²', 'showIf' => ['field' => 'immobilienart', 'values' => ['Wohnung', 'Haus']]], ['name' => 'ausstattung', 'type' => 'checkboxes', 'label' => 'Ausstattung', 'required' => true, 'options' => ['Balkon', 'Garage', 'Keller'], 'showIfField' => 'immobilienart', 'showIfValues' => 'Wohnung, Haus'], ['name' => 'schritt_grund', 'type' => 'step', 'label' => 'Grundstück', 'showIf' => ['field' => 'immobilienart', 'values' => ['Grundstück']]], ['name' => 'flaeche', 'type' => 'number', 'label' => 'Grundstücksfläche', 'required' => true, 'min' => 10, 'unit' => 'm²'], ['name' => 'schritt_kontakt', 'type' => 'step', 'label' => 'Kontakt'], ['name' => 'email', 'type' => 'email', 'label' => 'E-Mail', 'required' => true], ['name' => 'datenschutz', 'type' => 'consent', 'label' => 'Einverstanden'], ]; } private function settings(): array { return ['recipient' => 'deeg@example.org', 'subject' => 'Bewertung', 'replyToField' => 'email', 'success' => 'Danke.', 'summary' => true]; } public function testStepsConditionsAndNumbersAreNormalisedWhenSaving(): void { $this->forms->save('preisfinder', 'Preisfinder', $this->preisfinder(), $this->settings(), 0); $form = $this->forms->findBySlug('preisfinder'); $fields = array_column($form['fields'], null, 'name'); // Beide Schreibweisen der Bedingung landen in derselben gespeicherten Form. self::assertSame(['field' => 'immobilienart', 'values' => ['Wohnung', 'Haus']], $fields['wohnflaeche']['showIf']); self::assertSame(['field' => 'immobilienart', 'values' => ['Wohnung', 'Haus']], $fields['ausstattung']['showIf']); self::assertSame(['field' => 'immobilienart', 'values' => ['Grundstück']], $fields['schritt_grund']['showIf']); self::assertNull($fields['email']['showIf']); // Grenzen sind Zahlen, die Einheit bleibt; ein Schritt fragt nichts und ist nie Pflicht. self::assertEquals(10, $fields['wohnflaeche']['min']); self::assertEquals(2000, $fields['wohnflaeche']['max']); self::assertSame('m²', $fields['wohnflaeche']['unit']); self::assertNull($fields['flaeche']['max']); self::assertFalse($fields['schritt_objekt']['required']); self::assertTrue($form['settings']['summary']); self::assertTrue($this->forms->needsScript($form)); // Ein gewöhnliches Kontaktformular braucht das Skript nicht - es hat weder Schritte noch // Bedingungen, und ohne Zusammenfassung bleibt die Seite ohne JavaScript. $this->forms->save('kontakt', 'Kontakt', [['name' => 'email', 'type' => 'email', 'label' => 'E-Mail', 'required' => true]], ['recipient' => 'deeg@example.org'], 0); $kontakt = $this->forms->findBySlug('kontakt'); self::assertFalse($this->forms->needsScript($kontakt)); self::assertFalse($kontakt['settings']['summary']); self::assertNull($kontakt['fields'][0]['showIf'], 'Auch ohne Angabe kennt jedes Feld seine Schlüssel.'); } public function testABrokenDefinitionIsRefusedWithAReason(): void { $faelle = [ 'Bedingung auf ein unbekanntes Feld' => [ ['name' => 'a', 'type' => 'text', 'label' => 'A', 'showIfField' => 'art', 'showIfValues' => 'Haus'], ], 'Bedingung auf ein Feld, das erst danach kommt' => [ ['name' => 'a', 'type' => 'text', 'label' => 'A', 'showIfField' => 'art', 'showIfValues' => 'Haus'], ['name' => 'art', 'type' => 'select', 'label' => 'Art', 'options' => ['Haus', 'Wohnung']], ], 'Bedingung auf ein Textfeld' => [ ['name' => 'art', 'type' => 'text', 'label' => 'Art'], ['name' => 'a', 'type' => 'text', 'label' => 'A', 'showIfField' => 'art', 'showIfValues' => 'Haus'], ], 'Bedingung mit einem Wert, der keine Option ist' => [ ['name' => 'art', 'type' => 'select', 'label' => 'Art', 'options' => ['Haus', 'Wohnung']], ['name' => 'a', 'type' => 'text', 'label' => 'A', 'showIfField' => 'art', 'showIfValues' => 'Schloss'], ], 'Bedingung ohne Wert' => [ ['name' => 'art', 'type' => 'select', 'label' => 'Art', 'options' => ['Haus', 'Wohnung']], ['name' => 'a', 'type' => 'text', 'label' => 'A', 'showIfField' => 'art', 'showIfValues' => ''], ], 'Untergrenze über der Obergrenze' => [ ['name' => 'n', 'type' => 'number', 'label' => 'N', 'min' => '10', 'max' => '5'], ], 'Grenze, die keine Zahl ist' => [ ['name' => 'n', 'type' => 'number', 'label' => 'N', 'min' => 'zehn'], ], 'Optionsfelder mit einer Option' => [ ['name' => 'r', 'type' => 'radio', 'label' => 'R', 'options' => ['Ja']], ], 'Option mit Komma' => [ ['name' => 'r', 'type' => 'radio', 'label' => 'R', 'options' => ['ja, neu', 'ja, alt']], ], 'nur Schritte' => [ ['name' => 's', 'type' => 'step', 'label' => 'Schritt'], ], 'Bild für eine Option, die es nicht gibt' => [ ['name' => 'r', 'type' => 'radio', 'label' => 'R', 'options' => ['Haus', 'Wohnung'], 'images' => ['Schloss' => str_repeat('a', 32)]], ], 'Bild, das keine Kennung ist' => [ ['name' => 'r', 'type' => 'radio', 'label' => 'R', 'options' => ['Haus', 'Wohnung'], 'images' => 'Haus = bild.png'], ], ]; foreach ($faelle as $fall => $fields) { try { $this->forms->save('f', 'F', $fields, ['recipient' => 'a@example.org'], 0); self::fail($fall.' wurde angenommen.'); } catch (InvalidForm $e) { self::assertNotSame('', $e->getMessage(), $fall); } } } public function testOnlyFieldsWhoseConditionHoldsAreRequiredCheckedAndKept(): void { $this->forms->save('preisfinder', 'Preisfinder', $this->preisfinder(), $this->settings(), 0); $form = $this->forms->findBySlug('preisfinder'); // Ein Grundstück: Wohnfläche und Ausstattung gelten nicht - nicht verlangt, nicht // gespeichert, auch wenn der Browser etwas mitschickt. Der Grundstücksschritt gilt. $grund = $this->forms->check($form, ['immobilienart' => 'Grundstück', 'wohnflaeche' => '80', 'ausstattung' => ['Balkon'], 'flaeche' => '1250.00', 'email' => 'x@example.org', 'datenschutz' => 'ja']); self::assertSame([], $grund['errors']); self::assertSame('', $grund['values']['wohnflaeche']); self::assertSame('', $grund['values']['ausstattung']); self::assertSame('1250', $grund['values']['flaeche'], 'Eine Zahl wird ohne unnötige Stellen gespeichert.'); self::assertArrayNotHasKey('schritt_objekt', $grund['values'], 'Ein Schritt ist keine Antwort.'); // Eine Wohnung: jetzt sind Wohnfläche und Ausstattung Pflicht, der Grundstücksschritt nicht. $leer = $this->forms->check($form, ['immobilienart' => 'Wohnung', 'email' => 'x@example.org', 'datenschutz' => 'ja']); self::assertSame('Dieses Feld wird benötigt.', $leer['errors']['wohnflaeche']); self::assertSame('Bitte wähle mindestens eine Option.', $leer['errors']['ausstattung']); self::assertArrayNotHasKey('flaeche', $leer['errors']); // Zahlen mit Komma, außerhalb der Grenzen, keine Zahl; Mehrfachauswahl mit fremder Option. $wohnung = $this->forms->check($form, ['immobilienart' => 'Wohnung', 'wohnflaeche' => '85,5', 'ausstattung' => ['Balkon', 'Keller'], 'email' => 'x@example.org', 'datenschutz' => 'ja']); self::assertSame([], $wohnung['errors']); self::assertSame('85.5', $wohnung['values']['wohnflaeche']); self::assertSame('Balkon, Keller', $wohnung['values']['ausstattung']); self::assertSame('', $wohnung['values']['flaeche']); $zuViel = $this->forms->check($form, ['immobilienart' => 'Haus', 'wohnflaeche' => '5000', 'ausstattung' => ['Garage', 'Pool'], 'email' => 'x@example.org', 'datenschutz' => 'ja']); self::assertSame('Bitte gib einen Wert zwischen 10 und 2000 an.', $zuViel['errors']['wohnflaeche']); self::assertSame('Bitte wähle nur aus den angebotenen Optionen.', $zuViel['errors']['ausstattung']); self::assertSame('Garage', $zuViel['values']['ausstattung'], 'Die fremde Option bleibt weg.'); $keineZahl = $this->forms->check($form, ['immobilienart' => 'Grundstück', 'flaeche' => 'groß', 'email' => 'x@example.org', 'datenschutz' => 'ja']); self::assertSame('Bitte gib eine Zahl an.', $keineZahl['errors']['flaeche']); $zuKlein = $this->forms->check($form, ['immobilienart' => 'Grundstück', 'flaeche' => '3', 'email' => 'x@example.org', 'datenschutz' => 'ja']); self::assertSame('Bitte gib mindestens 10 an.', $zuKlein['errors']['flaeche']); // Eine Objektart, die es nicht gibt: abgelehnt, und nichts hängt davon ab. $fremd = $this->forms->check($form, ['immobilienart' => 'Schloss', 'email' => 'x@example.org', 'datenschutz' => 'ja']); self::assertSame('Bitte wähle eine der angebotenen Optionen.', $fremd['errors']['immobilienart']); self::assertArrayNotHasKey('wohnflaeche', $fremd['errors']); } public function testAStepFormIsRenderedAsFieldsetsAndBringsItsScript(): void { $this->forms->save('preisfinder', 'Preisfinder', $this->preisfinder(), $this->settings(), 0); $document = DemoContent::starter('Bewertung'); $document['schemaVersion'] = 23; $document['blocks'][] = ['id' => 'preis', 'type' => 'form', 'props' => ['form' => 'preisfinder']]; $this->pages->publish('page-1', $this->pages->saveDraft('page-1', $document, 0), 1); $antwort = $this->request('/'); $html = $antwort->getContent(); self::assertSame(200, $antwort->getStatusCode()); // Schritte sind Abschnitte mit Titel; ohne Skript stehen sie untereinander. self::assertSame(4, substr_count($html, '