From e85cc709826f3ee5651bfa6ebe2389b7e9eb3f9e Mon Sep 17 00:00:00 2001 From: Przemek Zatorski Date: Wed, 23 Sep 2026 09:10:20 +0200 Subject: [PATCH] Zapisane wyszukiwania na koncie zamiast w przegladarce Nowa tabela saved_searches i /api/saved-searches (lista, dodanie, usuniecie, jednorazowy import wpisow z localStorage). Front czyta liste z serwera, wiec wyszukiwania sa te same na kazdym urzadzeniu i przezywaja wyczyszczenie danych strony. Bez powiadomien. Skrypt czyszczacy usuwa wpisy skasowanych kont. Co-Authored-By: Claude Opus 5 (1M context) --- .../pl/polskalokalnie/search/SavedSearch.java | 193 ++++++++++++++++++ .../search/SavedSearchController.java | 151 ++++++++++++++ .../search/SavedSearchRepository.java | 11 + .../search/SavedSearchControllerTest.java | 38 ++++ frontend/src/App.tsx | 131 +++++++++--- frontend/tests/routing.spec.ts | 31 +++ scripts/cleanup-test-data.sql | 1 + 7 files changed, 532 insertions(+), 24 deletions(-) create mode 100644 backend/src/main/java/pl/polskalokalnie/search/SavedSearch.java create mode 100644 backend/src/main/java/pl/polskalokalnie/search/SavedSearchController.java create mode 100644 backend/src/main/java/pl/polskalokalnie/search/SavedSearchRepository.java create mode 100644 backend/src/test/java/pl/polskalokalnie/search/SavedSearchControllerTest.java diff --git a/backend/src/main/java/pl/polskalokalnie/search/SavedSearch.java b/backend/src/main/java/pl/polskalokalnie/search/SavedSearch.java new file mode 100644 index 0000000..9f7602f --- /dev/null +++ b/backend/src/main/java/pl/polskalokalnie/search/SavedSearch.java @@ -0,0 +1,193 @@ +package pl.polskalokalnie.search; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.time.Instant; + +/** + * Zapisane wyszukiwanie uzytkownika ("Zapisz wyszukiwanie" na liscie ofert). Wczesniej + * zylo tylko w localStorage przegladarki, wiec ginelo po zmianie urzadzenia albo wyczyszczeniu + * danych. Nie wysylamy z niego zadnych powiadomien - to lista do wygodnego wracania do filtrow. + */ +@Entity +@Table(name = "saved_searches") +public class SavedSearch { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "user_email", nullable = false, length = 180) + private String userEmail; + + /** "buy" albo "rent" - te same wartosci co w adresach /kupno i /wynajem. */ + @Column(nullable = false, length = 10) + private String mode; + + @Column(length = 120) + private String city; + + @Column(name = "property_type", length = 60) + private String propertyType; + + @Column(name = "price_min") + private Long priceMin; + + @Column(name = "price_max") + private Long priceMax; + + @Column(name = "area_min") + private Integer areaMin; + + @Column(name = "area_max") + private Integer areaMax; + + private Integer rooms; + + @Column(name = "secondary_market", nullable = false) + private boolean secondaryMarket; + + @Column(name = "primary_market", nullable = false) + private boolean primaryMarket; + + @Column(name = "no_fee_only", nullable = false) + private boolean noFeeOnly; + + /** Pozostale filtry ("wiecej filtrow") jako JSON - jest ich wiele i zmieniaja sie z widokiem. */ + @Column(name = "more_filters", columnDefinition = "text") + private String moreFilters; + + @Column(name = "created_at", nullable = false) + private Instant createdAt; + + @Column(name = "updated_at", nullable = false) + private Instant updatedAt; + + public Long getId() { + return id; + } + + public String getUserEmail() { + return userEmail; + } + + public void setUserEmail(String userEmail) { + this.userEmail = userEmail; + } + + public String getMode() { + return mode; + } + + public void setMode(String mode) { + this.mode = mode; + } + + public String getCity() { + return city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getPropertyType() { + return propertyType; + } + + public void setPropertyType(String propertyType) { + this.propertyType = propertyType; + } + + public Long getPriceMin() { + return priceMin; + } + + public void setPriceMin(Long priceMin) { + this.priceMin = priceMin; + } + + public Long getPriceMax() { + return priceMax; + } + + public void setPriceMax(Long priceMax) { + this.priceMax = priceMax; + } + + public Integer getAreaMin() { + return areaMin; + } + + public void setAreaMin(Integer areaMin) { + this.areaMin = areaMin; + } + + public Integer getAreaMax() { + return areaMax; + } + + public void setAreaMax(Integer areaMax) { + this.areaMax = areaMax; + } + + public Integer getRooms() { + return rooms; + } + + public void setRooms(Integer rooms) { + this.rooms = rooms; + } + + public boolean isSecondaryMarket() { + return secondaryMarket; + } + + public void setSecondaryMarket(boolean secondaryMarket) { + this.secondaryMarket = secondaryMarket; + } + + public boolean isPrimaryMarket() { + return primaryMarket; + } + + public void setPrimaryMarket(boolean primaryMarket) { + this.primaryMarket = primaryMarket; + } + + public boolean isNoFeeOnly() { + return noFeeOnly; + } + + public void setNoFeeOnly(boolean noFeeOnly) { + this.noFeeOnly = noFeeOnly; + } + + public String getMoreFilters() { + return moreFilters; + } + + public void setMoreFilters(String moreFilters) { + this.moreFilters = moreFilters; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/backend/src/main/java/pl/polskalokalnie/search/SavedSearchController.java b/backend/src/main/java/pl/polskalokalnie/search/SavedSearchController.java new file mode 100644 index 0000000..b02cf67 --- /dev/null +++ b/backend/src/main/java/pl/polskalokalnie/search/SavedSearchController.java @@ -0,0 +1,151 @@ +package pl.polskalokalnie.search; + +import java.time.Instant; +import java.util.List; +import java.util.Objects; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.server.ResponseStatusException; + +/** + * Zapisane wyszukiwania zalogowanego uzytkownika. Dostep wymaga logowania (regula + * anyRequest().authenticated() w SecurityConfig), a kazda operacja dotyczy wylacznie + * wpisow wlasciciela konta. Zadnych powiadomien - to tylko lista zapisanych filtrow. + */ +@RestController +@RequestMapping("/api/saved-searches") +public class SavedSearchController { + + /** Tyle samo, ile dotad miescil zapis w przegladarce - chroni przed zasypaniem listy. */ + static final int LIMIT = 60; + + private final SavedSearchRepository repository; + + public SavedSearchController(SavedSearchRepository repository) { + this.repository = repository; + } + + /** Kryteria wyszukiwania - dokladnie to, co pokazuje pasek filtrow na liscie ofert. */ + public record SavedSearchRequest(String mode, String city, String propertyType, Long priceMin, Long priceMax, + Integer areaMin, Integer areaMax, Integer rooms, boolean wtorny, boolean pierwotny, + boolean noFeeOnly, String more) { + } + + public record SavedSearchResponse(Long id, String mode, String city, String propertyType, Long priceMin, + Long priceMax, Integer areaMin, Integer areaMax, Integer rooms, boolean wtorny, + boolean pierwotny, boolean noFeeOnly, String more, Instant createdAt, + Instant updatedAt) { + } + + @GetMapping + public List list(Authentication authentication) { + return repository.findByUserEmailIgnoreCaseOrderByCreatedAtDesc(authentication.getName()).stream() + .map(SavedSearchController::toResponse) + .toList(); + } + + @PostMapping + @Transactional + public SavedSearchResponse add(@RequestBody SavedSearchRequest request, Authentication authentication) { + if (request == null || request.mode() == null || request.mode().isBlank()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Brak trybu wyszukiwania"); + } + String userEmail = authentication.getName(); + List existing = repository.findByUserEmailIgnoreCaseOrderByCreatedAtDesc(userEmail); + SavedSearch duplicate = existing.stream().filter(saved -> sameCriteria(saved, request)).findFirst().orElse(null); + if (duplicate != null) { + return toResponse(duplicate); + } + // Najstarsze wpisy ponad limit usuwamy, zeby zapis zawsze sie udal. + existing.stream().skip(LIMIT - 1L).forEach(repository::delete); + return toResponse(repository.save(fromRequest(request, userEmail))); + } + + /** + * Przeniesienie wyszukiwan zapisanych wczesniej w przegladarce. Dodajemy tylko te, + * ktorych uzytkownik nie ma jeszcze na koncie - import mozna powtorzyc bez duplikatow. + */ + @PostMapping("/import") + @Transactional + public List importLocal(@RequestBody List requests, Authentication authentication) { + String userEmail = authentication.getName(); + if (requests != null) { + for (SavedSearchRequest request : requests) { + if (request == null || request.mode() == null || request.mode().isBlank()) { + continue; + } + List current = repository.findByUserEmailIgnoreCaseOrderByCreatedAtDesc(userEmail); + if (current.size() >= LIMIT || current.stream().anyMatch(saved -> sameCriteria(saved, request))) { + continue; + } + repository.save(fromRequest(request, userEmail)); + } + } + return list(authentication); + } + + @DeleteMapping("/{id}") + @Transactional + public ResponseEntity remove(@PathVariable Long id, Authentication authentication) { + repository.findById(id) + .filter(saved -> saved.getUserEmail().equalsIgnoreCase(authentication.getName())) + .ifPresent(repository::delete); + return ResponseEntity.noContent().build(); + } + + private static SavedSearch fromRequest(SavedSearchRequest request, String userEmail) { + SavedSearch saved = new SavedSearch(); + Instant now = Instant.now(); + saved.setUserEmail(userEmail); + saved.setMode(request.mode()); + saved.setCity(request.city()); + saved.setPropertyType(request.propertyType()); + saved.setPriceMin(request.priceMin()); + saved.setPriceMax(request.priceMax()); + saved.setAreaMin(request.areaMin()); + saved.setAreaMax(request.areaMax()); + saved.setRooms(request.rooms()); + saved.setSecondaryMarket(request.wtorny()); + saved.setPrimaryMarket(request.pierwotny()); + saved.setNoFeeOnly(request.noFeeOnly()); + saved.setMoreFilters(request.more()); + saved.setCreatedAt(now); + saved.setUpdatedAt(now); + return saved; + } + + static boolean sameCriteria(SavedSearch saved, SavedSearchRequest request) { + return Objects.equals(saved.getMode(), request.mode()) + && Objects.equals(trim(saved.getCity()), trim(request.city())) + && Objects.equals(trim(saved.getPropertyType()), trim(request.propertyType())) + && Objects.equals(saved.getPriceMin(), request.priceMin()) + && Objects.equals(saved.getPriceMax(), request.priceMax()) + && Objects.equals(saved.getAreaMin(), request.areaMin()) + && Objects.equals(saved.getAreaMax(), request.areaMax()) + && Objects.equals(saved.getRooms(), request.rooms()) + && saved.isSecondaryMarket() == request.wtorny() + && saved.isPrimaryMarket() == request.pierwotny() + && saved.isNoFeeOnly() == request.noFeeOnly() + && Objects.equals(trim(saved.getMoreFilters()), trim(request.more())); + } + + private static String trim(String value) { + return value == null ? "" : value.trim(); + } + + private static SavedSearchResponse toResponse(SavedSearch saved) { + return new SavedSearchResponse(saved.getId(), saved.getMode(), saved.getCity(), saved.getPropertyType(), + saved.getPriceMin(), saved.getPriceMax(), saved.getAreaMin(), saved.getAreaMax(), saved.getRooms(), + saved.isSecondaryMarket(), saved.isPrimaryMarket(), saved.isNoFeeOnly(), saved.getMoreFilters(), + saved.getCreatedAt(), saved.getUpdatedAt()); + } +} diff --git a/backend/src/main/java/pl/polskalokalnie/search/SavedSearchRepository.java b/backend/src/main/java/pl/polskalokalnie/search/SavedSearchRepository.java new file mode 100644 index 0000000..687c46b --- /dev/null +++ b/backend/src/main/java/pl/polskalokalnie/search/SavedSearchRepository.java @@ -0,0 +1,11 @@ +package pl.polskalokalnie.search; + +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface SavedSearchRepository extends JpaRepository { + + List findByUserEmailIgnoreCaseOrderByCreatedAtDesc(String userEmail); + + void deleteByUserEmailIgnoreCase(String userEmail); +} diff --git a/backend/src/test/java/pl/polskalokalnie/search/SavedSearchControllerTest.java b/backend/src/test/java/pl/polskalokalnie/search/SavedSearchControllerTest.java new file mode 100644 index 0000000..ca31da9 --- /dev/null +++ b/backend/src/test/java/pl/polskalokalnie/search/SavedSearchControllerTest.java @@ -0,0 +1,38 @@ +package pl.polskalokalnie.search; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import pl.polskalokalnie.search.SavedSearchController.SavedSearchRequest; + +class SavedSearchControllerTest { + + private SavedSearch saved(String city, Long priceMax, String more) { + SavedSearch saved = new SavedSearch(); + saved.setMode("buy"); + saved.setCity(city); + saved.setPropertyType("Mieszkanie"); + saved.setPriceMax(priceMax); + saved.setSecondaryMarket(true); + saved.setMoreFilters(more); + return saved; + } + + private SavedSearchRequest request(String city, Long priceMax, String more) { + return new SavedSearchRequest("buy", city, "Mieszkanie", null, priceMax, null, null, null, true, false, false, more); + } + + @Test + void treatsSameCriteriaAsDuplicate() { + assertThat(SavedSearchController.sameCriteria(saved("Kraków", 800000L, "{}"), request("Kraków", 800000L, "{}"))).isTrue(); + // Spacje i brak wartosci nie robia z wyszukiwania nowego wpisu. + assertThat(SavedSearchController.sameCriteria(saved(" Kraków ", 800000L, null), request("Kraków", 800000L, ""))).isTrue(); + } + + @Test + void differentFiltersAreSeparateSearches() { + assertThat(SavedSearchController.sameCriteria(saved("Kraków", 800000L, "{}"), request("Kraków", 900000L, "{}"))).isFalse(); + assertThat(SavedSearchController.sameCriteria(saved("Kraków", 800000L, "{}"), request("Gdańsk", 800000L, "{}"))).isFalse(); + assertThat(SavedSearchController.sameCriteria(saved("Kraków", 800000L, "{}"), request("Kraków", 800000L, "{\"balkon\":\"tak\"}"))).isFalse(); + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ff6a670..ad46201 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -384,7 +384,71 @@ const CITY_PRICE_ALERTS_STORAGE_SUFFIX = 'city-price-alert-subscriptions'; const LISTING_PRICE_ALERTS_STORAGE_SUFFIX = 'listing-price-alert-subscriptions'; const CITY_PRICE_ALERT_SNAPSHOTS_STORAGE_SUFFIX = 'city-price-alert-snapshots'; const CITY_PRICE_ALERT_EVENTS_STORAGE_SUFFIX = 'city-price-alert-events'; +// Klucz zostaje tylko po to, zeby raz przeniesc na konto wyszukiwania zapisane w przegladarce +// przed przeniesieniem tej funkcji na serwer. const SAVED_SEARCHES_STORAGE_SUFFIX = 'saved-searches'; + +// Zapisane wyszukiwanie w postaci z API - id nadaje serwer. +type ApiSavedSearch = { + id: number; + mode: SavedSearchMode; + city: string | null; + propertyType: string | null; + priceMin: number | null; + priceMax: number | null; + areaMin: number | null; + areaMax: number | null; + rooms: number | null; + wtorny: boolean; + pierwotny: boolean; + noFeeOnly: boolean; + more: string | null; + createdAt: string; + updatedAt: string; +}; + +function fromApiSavedSearch(item: ApiSavedSearch): SavedSearchRecord { + let more: Record = {}; + try { + more = item.more ? JSON.parse(item.more) as Record : {}; + } catch { + more = {}; + } + return { + id: String(item.id), + mode: item.mode, + city: item.city ?? '', + propertyType: item.propertyType ?? '', + priceMin: item.priceMin, + priceMax: item.priceMax, + areaMin: item.areaMin, + areaMax: item.areaMax, + rooms: item.rooms, + wtorny: item.wtorny, + pierwotny: item.pierwotny, + noFeeOnly: item.noFeeOnly, + more, + createdAt: item.createdAt, + updatedAt: item.updatedAt, + }; +} + +function toSavedSearchPayload(search: SavedSearchRecord | SavedSearchDraft) { + return { + mode: search.mode, + city: search.city, + propertyType: search.propertyType, + priceMin: search.priceMin, + priceMax: search.priceMax, + areaMin: search.areaMin, + areaMax: search.areaMax, + rooms: search.rooms, + wtorny: search.wtorny, + pierwotny: search.pierwotny, + noFeeOnly: search.noFeeOnly, + more: JSON.stringify(search.more ?? {}), + }; +} // Lokalizacje, ktorych cene za m2 sprawdzil uzytkownik - najnowsza pierwsza, przechowywane per konto. const PRICE_HISTORY_MARKETS_SUFFIX = 'price-history-markets'; const PRICE_HISTORY_MARKETS_LIMIT = 5; @@ -2703,12 +2767,26 @@ function App() { navigate(ROUTES.buy); }; + // Zapisane wyszukiwania trzyma serwer (/api/saved-searches), wiec sa te same na kazdym + // urzadzeniu. Wpisy zapisane wczesniej w przegladarce przenosimy raz przy pierwszym wejsciu. useEffect(() => { if (!user) { setSavedSearchesState([]); return; } - setSavedSearchesState(readUserJsonStorage(user, SAVED_SEARCHES_STORAGE_SUFFIX, [])); + let cancelled = false; + const local = readUserJsonStorage(user, SAVED_SEARCHES_STORAGE_SUFFIX, []); + const load = local.length > 0 + ? apiFetch('/saved-searches/import', { method: 'POST', body: JSON.stringify(local.map(toSavedSearchPayload)) }) + .then((items) => { + writeUserJsonStorage(user, SAVED_SEARCHES_STORAGE_SUFFIX, []); + return items; + }) + : apiFetch('/saved-searches'); + load + .then((items) => { if (!cancelled) { setSavedSearchesState(items.map(fromApiSavedSearch)); } }) + .catch(() => { if (!cancelled) { setSavedSearchesState([]); } }); + return () => { cancelled = true; }; }, [user]); useEffect(() => { @@ -2717,12 +2795,6 @@ function App() { setListingPriceAlertSubscriptionsState(readUserJsonStorage(user, LISTING_PRICE_ALERTS_STORAGE_SUFFIX, [])); }, [user]); - useEffect(() => { - if (!user) { - return; - } - writeUserJsonStorage(user, SAVED_SEARCHES_STORAGE_SUFFIX, savedSearchesState); - }, [savedSearchesState, user]); useEffect(() => { writeUserJsonStorage(user, FAVORITE_LISTINGS_STORAGE_SUFFIX, favoriteListingsState); @@ -3141,20 +3213,19 @@ function App() { } const draft = buildSavedSearchDraft(mode, filters); - setSavedSearchesState((current) => { - const existing = current.find((item) => sameSavedSearchCriteria(item, draft)); - if (existing) { - return current.filter((item) => item.id !== existing.id); - } - const nowIso = new Date().toISOString(); - const next: SavedSearchRecord = { - id: `saved-search-${Date.now()}-${Math.random().toString(36).slice(2, 7)}`, - createdAt: nowIso, - updatedAt: nowIso, - ...draft, - }; - return [next, ...current].slice(0, SAVED_SEARCHES_LIMIT); - }); + const existing = savedSearchesState.find((item) => sameSavedSearchCriteria(item, draft)); + if (existing) { + removeSavedSearch(existing.id); + return; + } + apiFetch('/saved-searches', { method: 'POST', body: JSON.stringify(toSavedSearchPayload(draft)) }) + .then((saved) => { + const record = fromApiSavedSearch(saved); + setSavedSearchesState((current) => ( + current.some((item) => item.id === record.id) ? current : [record, ...current].slice(0, SAVED_SEARCHES_LIMIT) + )); + }) + .catch(() => { /* brak sieci - lista zostaje bez zmian, uzytkownik moze sprobowac ponownie */ }); }; const openSavedSearch = (savedSearch: SavedSearchRecord) => { @@ -3164,6 +3235,8 @@ function App() { const removeSavedSearch = (savedSearchId: string) => { setSavedSearchesState((current) => current.filter((item) => item.id !== savedSearchId)); + apiFetch(`/saved-searches/${encodeURIComponent(savedSearchId)}`, { method: 'DELETE' }) + .catch(() => { /* usuniecie na serwerze nie wyszlo - wroci przy nastepnym wejsciu */ }); }; const buySearchSaved = savedSearchesState.some((item) => sameSavedSearchCriteria(item, buildSavedSearchDraft('buy', filters))); @@ -6417,9 +6490,19 @@ function AccountSidebar() { return 0; } })(); - const savedSearchCount = user - ? readUserJsonStorage(user, SAVED_SEARCHES_STORAGE_SUFFIX, []).length - : 0; + // Zapisane wyszukiwania sa na koncie, wiec licznik bierzemy z serwera - tak jak liczbe ofert. + const [savedSearchCount, setSavedSearchCount] = useState(0); + useEffect(() => { + if (!user) { + setSavedSearchCount(0); + return; + } + let cancelled = false; + apiFetch('/saved-searches') + .then((items) => { if (!cancelled) { setSavedSearchCount(items.length); } }) + .catch(() => { if (!cancelled) { setSavedSearchCount(0); } }); + return () => { cancelled = true; }; + }, [user, location.pathname]); const favoritesCount = user ? readUserJsonStorage(user, FAVORITE_LISTINGS_STORAGE_SUFFIX, []).length : 0; diff --git a/frontend/tests/routing.spec.ts b/frontend/tests/routing.spec.ts index 199da44..e6fac2a 100644 --- a/frontend/tests/routing.spec.ts +++ b/frontend/tests/routing.spec.ts @@ -500,3 +500,34 @@ test.describe('Standard: przyciski robia to, co obiecuja', () => { await expect(page.locator('a.help-contact-channel-action[href^="tel:"]')).toHaveCount(1); }); }); + +// Zapisane wyszukiwania trzyma serwer, wiec sa te same w kazdej przegladarce i przezywaja +// wyczyszczenie danych strony. Bez powiadomien - to tylko lista filtrow do wygodnego powrotu. +test.describe('Zapisane wyszukiwania', () => { + test('zapis przezywa wyczyszczenie pamieci przegladarki', async ({ page }) => { + await login(page); + await goto(page, `${ROUTES.buy}?miasto=Gdynia`); + const saveButton = page.getByRole('button', { name: /wyszukiwanie/ }).first(); + if ((await saveButton.innerText()).includes('Zapisano')) { + await saveButton.click(); + await expect(saveButton).toHaveText(/Zapisz wyszukiwanie/); + } + await saveButton.click(); + await expect(saveButton).toHaveText(/Zapisano wyszukiwanie/); + + // Symulacja innego urzadzenia: zostaje sam token logowania, reszta pamieci znika. + await page.evaluate(() => { + const token = window.localStorage.getItem('polskalokalnie-auth-token'); + window.localStorage.clear(); + if (token) { + window.localStorage.setItem('polskalokalnie-auth-token', token); + } + }); + await goto(page, ROUTES.accountSearches); + await expect(page.locator('.account-searches-list, main').first()).toContainText('Gdynia'); + + // Sprzatanie, zeby kolejne uruchomienie testu zaczynalo od czystej listy. + await page.locator('.account-search-remove').first().click(); + await expect(page.locator('main')).not.toContainText('Gdynia'); + }); +}); diff --git a/scripts/cleanup-test-data.sql b/scripts/cleanup-test-data.sql index 4d22de9..79c850a 100644 --- a/scripts/cleanup-test-data.sql +++ b/scripts/cleanup-test-data.sql @@ -108,6 +108,7 @@ DELETE FROM notifications WHERE lower(user_email) NOT IN (SELECT email FROM k DELETE FROM auth_tokens WHERE lower(user_email) NOT IN (SELECT email FROM keep_emails); DELETE FROM phone_otps WHERE lower(user_email) NOT IN (SELECT email FROM keep_emails); DELETE FROM support_messages WHERE lower(account_email) NOT IN (SELECT email FROM keep_emails); +DELETE FROM saved_searches WHERE lower(user_email) NOT IN (SELECT email FROM keep_emails); DELETE FROM user_settings WHERE user_id NOT IN (SELECT id FROM keep_users); DELETE FROM app_users WHERE id IN (SELECT id FROM del_users);