माइग्रेशन Supabase → Firebase : बैकएंड के बिना फायरस्टोर पर संपर्क फ़ॉर्म जोड़ें
प्रकाशित: 29 April 2026
- दृश्य : एक फ़ॉर्म जो कुछ भी स्टोर नहीं करता
- फायरबेस सुपाबेस के बजाय क्यों उपयोग करें?
- चरण 1: Firebase प्रोजेक्ट बनाएँ
- फेज 2 : जावास्क्रिप्ट सबमिशन को फिर से लिखें
- चरण 3 : Supabase में मृत कोड को साफ़ करें
- फ़ेज 4: फ़ुटर कॉन्फ़िगर करें
- चरण 5 : अंतिम वास्तुकला
- इस प्रवासन की बात डॉगफूडिंग के बारे में
- आगामी कदम (backlog)
- संदर्भों
कई महीनों तक, इस साइट का संपर्क फ़ॉर्म एक mock JavaScript पर चल रहा था — 85 % de सफलता की एक वादा, एक faux Firestore, zéro données stockées। प्रारंभिक योजना ने एक Supabase बैकएंड की योजना बनाई थी, जिसमें Google Apps Script का उपयोग ईमेल सूचनाओं के लिए किया जाता था। त्याग दिया गया। आज, मैं Firebase Firestore में माइग्रेशन के बारे में बताता हूँ: projet की रचना, règles de sécurité, JS की फिर से लिखना, code mort Supabase की सफाई। और यह बताता है कि यह चयन विकास दर्शन के एक व्यापक पहलू को दर्शाता है।
दृश्य : एक फ़ॉर्म जो कुछ भी स्टोर नहीं करता
यह साइट JBake द्वारा उत्पन्न की गई है, मेरा Gradle प्लगइन`bakery`. यह 100% स्थैतिक है — कोई बैकएंड नहीं, कोई डेटाबेस नहीं। लेकिन मेरे पास एक संपर्क फॉर्म है। पृष्ठ`contact.html`मौजूद है, HTML तैयार है (फ़ील्ड: नाम, ईमेल, फ़ोन, विषय, संदेश, HTML5 मान्यता, हनीपोट एंटी‑स्पैम), Bootstrap स्टाइल्स लागू हैं। दृश्यतः, सब कुछ सही है।
सिवाय इसके कि जमा करने पर, कुछ नहीं होता।
const firebaseMock = new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() < 0.85) {
resolve({ status: 201, message: 'Message stored in Firestore.' });
} else {
reject({ status: 500, message: 'Firestore write failed.' });
}
}, 1500);
});
एक मॉक। एक वादा जो दिखावा करता है। उपयोगकर्ता एक स्पिनर देखता है, फिर एक संदेश « संदेश सफलतापूर्वक भेजा गया! » देखता है। लेकिन डेटा खाली में चला जाता है। कोई भी संदेश कहीं भी संग्रहीत नहीं है।
स्थिति एक टूटे हुए फॉर्म से भी बदतर है — यह एक फॉर्म है जो झूठ बोलता है।
Supabase का विरासत
प्रारंभिक योजना, में दस्तावेज़`content/draft/integration_formulaire_contact_supabase.adoc`, अपेक्षा कर रहा था :
-
Supabase के साथ एक टेबल`contacts`और Row Level Security
-
एक RPC`handle_contact_form`सर्वर पक्ष
-
एक एसक्यूएल ट्रिगर जो एक गूगल एप्स स्क्रिप्ट वेबहुक को बुलाता है
-
Google Apps Script जो एक Gmail अधिसूचना ईमेल भेजता है
संबंधित JavaScript कोड अभी भी … में मौजूद है`script.js`। वहाँ एक कक्षा है`SupabaseManager`जो वैश्विक चर के साथ एक Supabase क्लाइंट प्रारंभ करता है`SUPABASE_URL` et SUPABASE_KEY, और एक कक्षा`ContactFormHandler`जो फ़ॉर्म के सबमिट इवेंट को सुनता है और बुलाता है`SupabaseManager.submitContactForm()`.
समस्या : ये ग्लोबल चर अब फुटर में इंजेक्ट नहीं किए जाते। ले`<script src="supabase-js">`हटाया गया। कोड बुलाता है`supabase.createClient()`एक चर पर`supabase`जिसका अस्तित्व अब नहीं है। इसलिए:
console.error : 'Supabase client library (supabase-js) is not loaded.'
न केवल डेटा संग्रहीत नहीं किए जाते हैं, बल्कि सबमिशन कोड मृत है।
भूतिया दोगुना जमा
बदतर करने के लिए, एक हैशांत प्रतियोगिताएक ही फॉर्म पर दो हैंडलर्स के बीच :
-
`contact.js`सबमिट को सुनें, मॉक फायरबेस को बुलाएँ
-
script.js— के माध्यम से`ContactFormHandler`— सबमिट को भी सुनें, बुलाएँ`SupabaseManager`
दोनों करते हैं`event.preventDefault() + event.stopPropagation(). जैसे`contact.js`पहले लोड किया जाता है में`footer.thyme, इसका हैंडलर पहले से जुड़ा है। यह प्रसार को रोकता है।`ContactFormHandler`कभी ट्रिगर नहीं किया जाएगा।
यह सक्रिय बग भी नहीं है — यह एक ज़ोंबी है। ऐसा कोड जो कभी चलने का मौका नहीं मिलता।
@startuml skinparam backgroundColor #FEFEFE title प्रारंभिक स्थिति — संपर्क फॉर्म rectangle "contact.thyme\n(HTML Bootstrap, honeypot)" as FORM rectangle "footer.thyme\n(Firebase SDK के साथ कॉन्फ़िगरेशन प्लेसहोल्डर)" as FOOT rectangle "contact.js\n(mock Firebase, 85% सफलता)" as CONT rectangle "script.js\n(SupabaseManager + ContactFormHandler, मृत)" as SCRIPT rectangle "उपयोगकर्ता" as USER USER -> FORM : फ़ॉर्म जमा FORM -> CONT : जमा इवेंट (पहले संलग्न) CONT -> CONT : preventDefault + stopPropagation CONT -> CONT : mock Promise (1.5s, यादृच्छिक) note right of CONT ⚠️ कोई डेटा संग्रहीत नहीं end note FORM ..> SCRIPT : इवेंट जमा करें (ब्लॉक किया गया stopPropagation) note right of SCRIPT : ❌ कभी ट्रिगर नहीं किया गया note right of SCRIPT : ❌ supabase-js लोड नहीं हुआ @enduml
फायरबेस सुपाबेस के बजाय क्यों उपयोग करें?
माइग्रेशन निर्णय को _ में दस्तावेज़ किया गया है`AGENT.adoc`(There is no text to translate, so output nothing.)
Firebase अब निम्नलिखित कारणों से चुना जाता है: बेहतर निःशुल्क योजना, नैसर्गिक Firestore, एकीकृत Cloud Functions, Google का अधिक अनुकूल इकोसिस्टम। मौजूदा Supabase कार्यान्वयन को « ⚠️ त्यागा गया » के रूप में चिह्नित किया गया है।
मुफ्त योजना से परे, एक वास्तुशिल्प कारण है। यह साइट Google इकोसिस्टम में रहती है: लक्ष्य भंडार है।cheroliv.github.io, CNAME GitHub Pages पर इशारा करता है, Gradle बिल्ड JGit के माध्यम से GitHub पर पुश किया जाता है. गूगल सेवा (Firebase) को जोड़ने से तृतीय-पक्ष सेवा (Supabase) की तुलना में फैलाव की सतह कम हो जाती है.
Firestore नेटिव मोड (Datastore मोड नहीं) में यह भी मेरे दिमाग में NoSQL दस्तावेज़ मानसिक मॉडल के अधिक निकट है : संग्रह, दस्तावेज़, टाइप्ड फ़ील्ड, सर्वर टाइमस्टैम्प, अंतर्निहित सुरक्षा नियम।
चरण 1: Firebase प्रोजेक्ट बनाएँ
प्रारम्भ
मेरी मशीन पर Firebase CLI स्थापित नहीं है, इसलिए मैं वेब कंसोल का उपयोग कर रहा हूँ :
-
जाना परhttps://console.firebase.google.com/[फ़ायरबेस कंसोल]
-
एक परियोजना बनाएँ`cheroliv-contact`(या मौजूदा प्रोजेक्ट का पुन: उपयोग करें)
-
फ़ायरस्टोर को नेटिव मोड में सक्रिय करें (Datastore नहीं)
-
क्षेत्र में डेटाबेस बनाएँ`eur3`(यूरोप)
हमारे जैसे न्यूनतम उपयोग के लिए (एक ही संग्रह, सार्वजनिक लेखन), नैटिव मोड सही विकल्प है। जटिल Datastore नियमों की आवश्यकता नहीं है।
फ़ायरस्टोर सुरक्षा नियम
फ़ॉर्म सार्वजनिक है — कोई भी एक संदेश भेज सकता है। लेकिन मैं दुरुपयोग को सीमित करना चाहता हूँ :
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /contact_messages/{messageId} {
// Lecture : admin uniquement (authentifié)
allow read: if request.auth != null;
// Écriture : publique, mais limitée
allow create: if request.auth == null
&& request.resource.data.name is string
&& request.resource.data.name.size() >= 1
&& request.resource.data.name.size() <= 100
&& request.resource.data.email is string
&& request.resource.data.email.matches('.*@.*\\..*')
&& request.resource.data.email.size() <= 254
&& request.resource.data.subject is string
&& request.resource.data.subject.size() >= 3
&& request.resource.data.subject.size() <= 200
&& request.resource.data.message is string
&& request.resource.data.message.size() >= 10
&& request.resource.data.message.size() <= 5000
&& request.resource.data.created_at == request.time
&& request.resource.data.user_agent is string
&& request.resource.data.user_agent.size() <= 500;
}
}
}
मुख्य बिंदु:
-
allow read— केवल प्रमाणित उपयोगकर्ता संदेश पढ़ सकते हैं (मैं, फायरबेस कंसोल के माध्यम से) -
allow create— कोई भी दस्तावेज़ बना सकता है, लेकिन फ़ील्ड सत्यापन के साथ -
सर्वर‑साइड सत्यापन : न्यूनतम/अधिकतम आकार, ई‑मेल प्रारूप,
created_at`के अनुसार होना चाहिए`request.time(जालसाजी विरोधी) -
`user_agent`ट्रेसबिलिटी के लिए भेजा जाता है (गंभीर नहीं लेकिन उपयोगी)
ये नियम एक साधारण से भी अधिक कड़े हैं।allow write: if true;. वे एक हमलावर को विशाल पेलोड इंजेक्ट करने या खराब रूप से बने फ़ील्ड को इंजेक्ट करने से रोकते हैं।
फेज 2 : जावास्क्रिप्ट सबमिशन को फिर से लिखें
अनुबंध सरल है:
-
फ़ॉर्म डेटा पढ़ें
-
जाँचना हनीपोट (फ़ील्ड`hp_name`— यदि यह भरा है, तो यह एक बॉट है, हम कुछ भी भेजे बिना सफलता का संमुलन करते हैं)
-
बुलाना`addDoc(window.FIREBASE.collection(db, "contact_messages"), {…})`
-
सफलता या त्रुटि दिखाएँ
निर्भरता `window.FIREBASE
में`footer.thyme`, एक स्क्रिप्ट मॉड्यूल Firebase SDK को प्रारंभ करता है और एक वैश्विक ऑब्जेक्ट उजागर करता है :
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.6.0/firebase-app.js";
import { getFirestore, collection, addDoc, serverTimestamp }
from "https://www.gstatic.com/firebasejs/11.6.0/firebase-firestore.js";
const firebaseConfig = { /* valeurs réelles */ };
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
window.__FIREBASE__ = { db, collection, addDoc, serverTimestamp };
</script>
मॉड्यूल स्क्रिप्ट्स पहले चलते हैं`DOMContentLoaded`, इसलिए`window.FIREBASE`हैंडलर के समय यह उपलब्ध होने की गारंटी है`contact.js`यह ट्रिगर हो जाता है। सावधानी के रूप में, मैं एक 5 सेकंड का पोलिंग भी जोड़ता हूं, यदि CDN धीमा हो।
नया `contact.js
document.addEventListener('DOMContentLoaded', function () {
'use strict';
const form = document.getElementById('contact-form');
if (!form) return;
const submitButton = form.querySelector('button[type="submit"]');
const successMessage = document.getElementById('contact-success-message');
const errorMessage = document.getElementById('contact-error-message');
// Éléments de validation
const nameInput = form.querySelector('input[name="name"]');
const emailInput = form.querySelector('input[name="email"]');
const phoneInput = form.querySelector('input[name="phone"]');
const subjectInput = form.querySelector('input[name="subject"]');
const messageInput = form.querySelector('textarea[name="message"]');
const honeypotInput = form.querySelector('input[name="hp_name"]');
/**
* Attend que window.__FIREBASE__ soit disponible.
* Timeout de 5 secondes — si le CDN Firebase est lent, on abandonne.
*/
function waitForFirebase(timeoutMs = 5000) {
return new Promise((resolve, reject) => {
if (window.__FIREBASE__) {
resolve(window.__FIREBASE__);
return;
}
const start = Date.now();
const interval = setInterval(() => {
if (window.__FIREBASE__) {
clearInterval(interval);
resolve(window.__FIREBASE__);
} else if (Date.now() - start > timeoutMs) {
clearInterval(interval);
reject(new Error('Firebase SDK non disponible après timeout'));
}
}, 100);
});
}
// --- Validation (identique à l'existant) ---
function validateForm() {
nameInput.setCustomValidity('');
emailInput.setCustomValidity('');
if (phoneInput) phoneInput.setCustomValidity('');
subjectInput.setCustomValidity('');
messageInput.setCustomValidity('');
if (nameInput.value.trim().length < 1) {
nameInput.setCustomValidity('Veuillez saisir votre nom.');
}
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailPattern.test(emailInput.value.trim())) {
emailInput.setCustomValidity('Veuillez saisir une adresse email valide.');
}
if (phoneInput && phoneInput.value.trim() !== '') {
const phonePattern = /^\d{10,15}$/;
if (!phonePattern.test(phoneInput.value.trim())) {
phoneInput.setCustomValidity('Veuillez saisir un numéro valide (10 à 15 chiffres).');
}
}
if (subjectInput.value.trim().length < 3) {
subjectInput.setCustomValidity('Veuillez saisir un sujet (3 caractères minimum).');
}
if (messageInput.value.trim().length < 10) {
messageInput.setCustomValidity('Veuillez saisir un message (10 caractères minimum).');
}
form.classList.add('was-validated');
return form.checkValidity();
}
// --- Handler de soumission ---
form.addEventListener('submit', async function (event) {
event.preventDefault();
event.stopPropagation();
if (!validateForm()) return;
// Honeypot : si rempli, simuler un succès sans rien envoyer
if (honeypotInput && honeypotInput.value.trim() !== '') {
successMessage.style.display = 'block';
form.reset();
form.classList.remove('was-validated');
return;
}
// UI : état d'envoi
submitButton.disabled = true;
submitButton.innerHTML = `
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
Envoi en cours...
`;
successMessage.style.display = 'none';
errorMessage.style.display = 'none';
try {
const fb = await waitForFirebase();
const messagesCollection = fb.collection(fb.db, 'contact_messages');
await fb.addDoc(messagesCollection, {
name: nameInput.value.trim(),
email: emailInput.value.trim(),
phone: phoneInput ? phoneInput.value.trim() : '',
subject: subjectInput.value.trim(),
message: messageInput.value.trim(),
created_at: fb.serverTimestamp(),
user_agent: navigator.userAgent.substring(0, 500)
});
successMessage.style.display = 'block';
form.reset();
form.classList.remove('was-validated');
} catch (error) {
console.error('Erreur Firestore:', error);
errorMessage.style.display = 'block';
} finally {
submitButton.disabled = false;
submitButton.innerHTML = `
<i class="bi bi-send me-2"></i>
Envoyer le Message
`;
}
}, false);
});
मॉक के सापेक्ष बदलाव :
-
waitForFirebase()— टाइमआउट के साथ पोलिंग, मजबूत भले ही CDN धीमा हो -
honeypot— यदि छिपा हुआ फ़ील्ड`hp_name`भरा हुआ है, एक निर्णायक सफलता का अनुकरण Firestore. बॉट को लगता है कि वह सफल हो गया है लेकिन कुछ भी संग्रहीत नहीं है। -
addDoc(collection, {…})— वास्तविक कॉल Firestore के साथ`serverTimestamp()` etuser_agent -
त्रुटि प्रबंधन के साथ`try/catch`असिंक्रोनस
-
सफाई का`finally`बटन की बहाली
|
क्यों`user_agent`? यह वैकल्पिक है, लेकिन निदान के लिए उपयोगी है। यदि एक अजीब संदेश आता है, तो यह जानना कि यह डेस्कटॉप ब्राउज़र, मोबाइल या cURL स्क्रिप्ट से आया है, ट्राइज़ में मदद करता है। |
चरण 3 : Supabase में मृत कोड को साफ़ करें
`script.js`इसमें 250 dead code की लाइनों हैं:
-
SupabaseManager(लाइनों 417-481) — 65 लाइनों -
ContactFormHandler(पंक्तियाँ 490-551) — 62 पंक्तियाँ -
प्रारंभिक ब्लॉक (पंक्तियाँ 645-654) — 10 पंक्तियाँ
कुल : ~140 लाइनें हटाने के लिए।
ब्लॉक`DOMContentLoaded`बनाया एक`SupabaseManager`फिर एक`ContactFormHandler`फ़ॉर्म से जुड़ा। जैसा कि ऊपर बताया गया है, यह कोड कभी निष्पादित नहीं होता (ब्लॉक किया गया`contact.js`), और यदि वह चल रहा होता, तो यह विफल हो जाता (कोई Supabase SDK लोड नहीं किया गया)
मैं हटाता हूँ :
-
कक्षा`SupabaseManager`
-
कक्षा`ContactFormHandler`
-
प्रारम्भ ब्लॉक में`DOMContentLoaded`(लाइनों 645-654)
Le reste de script.js`अखंड है :`ThemeManager, ScrollToTopButton, MobileMenuManager, SmoothScrollWithOffset, NavbarHeightUpdater, DynamicNavbarBreakpoint, CodeBlockManager, TooltipManager, PhoneInputManager.
फ़ेज 4: फ़ुटर कॉन्फ़िगर करें
`footer.thyme`पहले से ही Firebase boilerplate मौजूद है लेकिन प्लेसहोल्डर मानों के साथ। मैं बदल रहा हूँ :
const firebaseConfig = {
apiKey: "REMPLACER_PAR_VOTRE_API_KEY",
authDomain: "REMPLACER_PAR_VOTRE_AUTH_DOMAIN",
projectId: "REMPLACER_PAR_VOTRE_PROJECT_ID",
storageBucket: "REMPLACER_PAR_VOTRE_STORAGE_BUCKET",
messagingSenderId: "REMPLACER_PAR_VOTRE_SENDER_ID",
appId: "REMPLACER_PAR_VOTRE_APP_ID"
};
वास्तविक मानों को पुनः प्राप्त करने सेप्रोजेक्ट सेटिंग्स > सामान्य > आपके ऐप्स > वेब ऐपफ़ायरबेस कंसोल में.
मान संवेदनशील हैं (apiKey`फ़ायरबेस में यह डिज़ाइन द्वारा सार्वजनिक है, लेकिन मैं स्पष्ट रूप में उन्हें कमिट नहीं करना पसंद करता/पसंद करती हूँ)। मैं उन्हें में संग्रहीत करता/करता हूँ`site.yml(पहले से`.gitignore`) और प्लगइन`bakery`वे उन्हें टेम्प्लेट में इंजेक्ट करते हैं, बिल्ड पक्ष में जोड़ने के लिए एक तर्क के माध्यम से।
अभी के लिए, मैं उन्हें सीधे इसमें डालता हूँ`footer.thyme`— बिल्ड`./gradlew serve`इसे स्थानीय रूप से लोड किया जाएगा। प्रतिनियुक्ति पर, मैं इंजेक्शन को आगे की ओर स्थानांतरित करूँगा।`site.yml`या Gradle चर की ओर
|
L'`apiKey`Firebase नहींनहींएक रहस्य। इसका डिज़ाइन से यह सार्वजनिक है। जो आपके डेटा की रक्षा करता है, वे हैंफ़ायरस्टोर सुरक्षा नियम, एपीआई की कुंजी नहीं। इसे एक में न रखें`.env`सर्वर-साइड पर लोड — यह ब्राउज़र में उजागर करने के लिए इरादा है। |
चरण 5 : अंतिम वास्तुकला
@startuml
skinparam backgroundColor #FEFEFE
title अंतिम वास्तुकला — संपर्क फ़ॉर्म Firebase
actor Utilisateur as USER
package "ब्राउज़र" #E8F5E9 {
rectangle "contact.thyme\n(HTML, honeypot)" as FORM
rectangle "contact.js\n(validation + Firestore)" as CONT
rectangle "footer.thyme\n(Firebase SDK init)" as FOOT
}
cloud "Firebase" #BBDEFB {
rectangle "Firestore\n(contact_messages)" as FS
}
FORM --> CONT : सबमिट इवेंट
CONT --> FOOT : window.__FIREBASE__.addDoc()
FOOT --> FS : document\n(सत्यापित सुरक्षा नियम)
note right of FS
Firestore नियम :
- create: public, सत्यापित
- read: केवल auth
end note
@enduml
इस प्रवासन की बात डॉगफूडिंग के बारे में
यह साइट मेरे निज़ी Gradle प्लगइन द्वारा उत्पन्न की जाती है।bakery. संपर्क फॉर्म साइट के भीतर रहता है। Supabase → Firebase माइग्रेशन का दस्तावेज़ीकरण … में किया गया है`AGENT.adoc`, यह बैकलॉग में चर्चा की जाती है, इसे के माध्यम से परीक्षण किया जाता है`./gradlew serve`, और यह एक ब्लॉग पोस्ट बनाता है (जो आप पढ़ रहे हैं).
यह शुद्ध डॉगफ़ूडिंग है। वेबसाइट प्लगिन का उत्पाद है, प्लगिन डेवलपर का उत्पाद है, डेवलपर प्रक्रिया को स्वयं साइट में दस्तावेज़ करता है।
लूप बंद है।
कई महीनों तक एक मॉक को खींच कर रखना (पूरे सत्रों में जहां फॉर्म चुपचाप झूठ बोल रहा था) ने मुझे यह एहसास दिलाया कि : एक व्यक्तिगत स्थिर वेबसाइट का बैकलॉग कभी « पूरा » नहीं होता। हमेशा एक प्राथमिक यूएस (यूजर स्टोरी) होती है, हमेशा एक ड्राफ्ट में लेख होता है, हमेशा एक टेम्प्लेट में एक टिप्पणी की गई खंड होता है। अनुशासन का मतलब सब कुछ पूरा करना नहीं है — यह उपयोगकर्ता के लिए दृश्यमान चीज़ों को पूरा करना है।
एक टूटा हुआ संपर्क फ़ॉर्म, संपर्क फ़ॉर्म न होने से भी बदतर है। यह एक अधूरा वादा है।
परिवर्तनों का सारांश
| फ़ाइल | संशोधन | प्रभाव |
|---|---|---|
|
लेख का निर्माण |
दस्तावेजीकरण |
|
रीराइटिंग (mock → वास्तविक Firestore) |
कार्यात्मक |
|
हटाना SupabaseManager + ContactFormHandler + init bloc |
सफ़ाई |
|
कॉन्फिग प्लेसहोल्डर का प्रतिस्थापन → वास्तविक मान |
कॉन्फ़िगरेशन |
आगामी कदम (backlog)
-
सूचना ईमेल: एक क्लाउड फ़ंक्शन`onCreate`पर`contact_messages`जो SendGrid के माध्यम से एक मेल भेजता है। फॉर्म स्टोर करता है, लेकिन मुझे सूचना नहीं मिलती। मध्यम प्राथमिकता — संदेश Firebase कंसोल में दृश्यमान हैं।
-
Rate limiting क्लाइंट साइड: लोकल स्टोरेज टाइमस्टैम्प जोड़ें ताकि बर्स्ट में कई सबमिशन को रोका जा सके। हनीपॉट नाइव बॉट्स को ब्लॉक करता है, एक रेट लिमिटर थोड़ा अधिक चालाक बॉट्स को ब्लॉक कर देगा।
-
टेस्टएक Playwright परीक्षण जो फ़ॉर्म सबमिट करता है और जाँचता है कि दस्तावेज़ Firestore में दिखाई दे रहा है। फिलहाल, मैं मैन्युअल रूप से vía परीक्षण कर रहा हूँ`./gradlew serve`.