diff --git a/.circleci/config.yml b/.circleci/config.yml index d2290bfacd..8a5e1ef5c7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -194,15 +194,20 @@ jobs: JEST_JUNIT_OUTPUT_DIR: ./reports/junit/ - store_test_results: path: ./reports/junit/ + - store_artifacts: + path: app/javascript/test-coverage unit-rspec: <<: *defaults steps: - setup + - run: wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | sudo apt-key add - - run: sudo apt-get update -y - run: sudo apt-get install graphicsmagick - run: RAILS_ENV=test bundle exec rails db:create - run: RAILS_ENV=test bundle exec rails db:test:prepare - run: bundle exec rake spec + - store_artifacts: + path: coverage unit-angular: <<: *defaults steps: diff --git a/.env.sample b/.env.sample index 184bf44959..9e5e6cb75c 100644 --- a/.env.sample +++ b/.env.sample @@ -1,7 +1,4 @@ EASYPOST_API_KEY='' -SFGOV_DATA_USERNAME='' -SFGOV_DATA_PASSWORD='' -SFGOV_DATA_APP_TOKEN='' LOCALHOST='' GOOGLE_TAG_MANAGER_KEY='' S3_ACCESS_KEY='' @@ -16,7 +13,7 @@ SALESFORCE_PASSWORD='' SALESFORCE_SECURITY_TOKEN='' SALESFORCE_CLIENT_SECRET='' SALESFORCE_CLIENT_ID='' -SALESFORCE_HOST='test.salesforce.com' +SALESFORCE_HOST='' SALESFORCE_INSTANCE_URL='' S3_BUCKET='' RESOURCE_URL='' diff --git a/.eslintrc.js b/.eslintrc.js index c4f57aa965..8497844ba9 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -8,6 +8,8 @@ module.exports = { globals: { Atomics: "readonly", SharedArrayBuffer: "readonly", + cy: "readonly", + Cypress: "readonly", }, // Specifies the ESLint parser parser: "@typescript-eslint/parser", diff --git a/.gitignore b/.gitignore index 3fe197b975..f6bc4c6db6 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,7 @@ yarn-debug.log* .DS_Store # Test output +coverage app/javascript/test-coverage junit.xml cypress/screenshots/* diff --git a/Gemfile b/Gemfile index 984e41bd13..adfaef84f1 100644 --- a/Gemfile +++ b/Gemfile @@ -23,7 +23,7 @@ gem "sprockets-rails" gem 'sprockets_uglifier_with_source_maps' # See https://github.com/rails/execjs#readme for more supported runtimes # gem 'therubyracer', platforms: :ruby -gem 'puma', '~> 5.6.8' +gem 'puma', '~> 6.4.2' # Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder gem 'jbuilder', '~> 2.8.0' diff --git a/Gemfile.lock b/Gemfile.lock index 272b5ef78a..3cb962f498 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -255,11 +255,11 @@ GEM net-protocol newrelic_rpm (9.0.0) nio4r (2.7.0) - nokogiri (1.14.2-arm64-darwin) + nokogiri (1.16.2-arm64-darwin) racc (~> 1.4) - nokogiri (1.14.2-x86_64-darwin) + nokogiri (1.16.2-x86_64-darwin) racc (~> 1.4) - nokogiri (1.14.2-x86_64-linux) + nokogiri (1.16.2-x86_64-linux) racc (~> 1.4) oj (3.14.2) oj_mimic_json (1.0.1) @@ -286,9 +286,9 @@ GEM pry (>= 0.10.4) psych (3.3.4) public_suffix (5.0.1) - puma (5.6.8) + puma (6.4.2) nio4r (~> 2.0) - racc (1.6.2) + racc (1.7.3) rack (2.2.6.4) rack-cors (1.0.6) rack (>= 1.6.0) @@ -521,7 +521,7 @@ DEPENDENCIES pry-byebug pry-rails psych (< 4) - puma (~> 5.6.8) + puma (~> 6.4.2) rack (>= 2.2.3) rack-cors (~> 1.0.5) rack-rewrite (~> 1.5.0) diff --git a/app/assets/javascripts/listings/ListingPreferenceService.js.coffee b/app/assets/javascripts/listings/ListingPreferenceService.js.coffee index dd844ad7b7..f68e9e435e 100644 --- a/app/assets/javascripts/listings/ListingPreferenceService.js.coffee +++ b/app/assets/javascripts/listings/ListingPreferenceService.js.coffee @@ -65,6 +65,10 @@ ListingPreferenceService = ($http, ListingConstantsService, ListingIdentityServi !_.invert(Service.preferenceMap)[listingPref.preferenceName] customProofPreferences = _.remove customPreferences, (customPref) -> _.includes(Service.hardcodeCustomProofPrefs, customPref.preferenceName) + + # custom preferences related to Veterans should not be seen by applicants + _.remove(customPreferences, (pref) -> _.includes(pref.preferenceName?.toLowerCase(), "veteran")) + listing.customPreferences = _.sortBy customPreferences, (pref) -> pref.order listing.customProofPreferences = _.sortBy customProofPreferences, (pref) -> pref.order diff --git a/app/assets/javascripts/shared/SharedService.js.coffee b/app/assets/javascripts/shared/SharedService.js.coffee index 5a983d3dc5..de39bbde5d 100644 --- a/app/assets/javascripts/shared/SharedService.js.coffee +++ b/app/assets/javascripts/shared/SharedService.js.coffee @@ -171,7 +171,7 @@ SharedService = ($http, $state, $window, $document) -> $window.VETERANS_APPLICATION_QUESTION is 'true' && !!listing.Listing_Lottery_Preferences && _.some(listing.Listing_Lottery_Preferences, (pref) -> - pref?.Lottery_Preference?.Name?.toLowerCase().includes("veteran") + _.includes(pref?.Lottery_Preference?.Name?.toLowerCase(), "veteran") ) return Service diff --git a/app/assets/javascripts/short-form/ShortFormApplicationController.js.coffee b/app/assets/javascripts/short-form/ShortFormApplicationController.js.coffee index c5f0f0b5ee..62cc57782b 100644 --- a/app/assets/javascripts/short-form/ShortFormApplicationController.js.coffee +++ b/app/assets/javascripts/short-form/ShortFormApplicationController.js.coffee @@ -718,19 +718,13 @@ ShortFormApplicationController = ( $scope.preferences.veterans_household_member = null $scope.checkAfterVeteransPreference = -> - # We don't want to show custom-preference page at all right now, because of the new combo-preferences in salesforce - # We might want to re-enable them in the future - # $scope.checkForCustomPreferences() - $scope.checkIfNoPreferencesSelected() + $scope.checkForCustomPreferences() $scope.checkAfterPreferencesPrograms = -> if $scope.showVeteransApplicationQuestion() ShortFormNavigationService.goToApplicationPage('dahlia.short-form-application.veterans-preference') else - # We don't want to show custom-preference page at all right now, because of the new combo-preferences in salesforce - # We might want to re-enable them in the future - # $scope.checkForCustomPreferences() - $scope.checkIfNoPreferencesSelected() + $scope.checkForCustomPreferences() ########## END VETERANS PREFERENCE LOGIC ########## diff --git a/app/assets/javascripts/short-form/ShortFormNavigationService.js.coffee b/app/assets/javascripts/short-form/ShortFormNavigationService.js.coffee index 98fdc9a2d1..6b081dcc9e 100644 --- a/app/assets/javascripts/short-form/ShortFormNavigationService.js.coffee +++ b/app/assets/javascripts/short-form/ShortFormNavigationService.js.coffee @@ -405,10 +405,8 @@ ShortFormNavigationService = ( when 'review-optional' if ShortFormApplicationService.applicantHasNoPreferences() 'general-lottery-notice' - # We don't want to show custom-preference page at all right now, because of the new combo-preferences in salesforce - # We might want to re-enable them in the future - # else if Service.hasCustomPreferences() - # 'custom-preferences' + else if Service.hasCustomPreferences() + 'custom-preferences' else if !Service.showVeteransApplicationQuestion() 'preferences-programs' else @@ -493,18 +491,12 @@ ShortFormNavigationService = ( "custom-proof-preferences({prefIdx: #{currentIndex - 1}})" Service.getPrevPageOfGeneralLottery = -> - # We don't want to show custom-preference page at all right now, because of the new combo-preferences in salesforce - # We might want to re-enable them in the future - # customProofPreferences = ShortFormApplicationService.listing.customProofPreferences - # if customProofPreferences.length - # "custom-proof-preferences({prefIdx: #{customProofPreferences.length - 1}})" - # else if Service.hasCustomPreferences() - # 'custom-preferences' - # else if !Service.showVeteransApplicationQuestion() - # 'preferences-programs' - # else - # 'veterans-preference' - if !Service.showVeteransApplicationQuestion() + customProofPreferences = ShortFormApplicationService.listing.customProofPreferences + if customProofPreferences.length + "custom-proof-preferences({prefIdx: #{customProofPreferences.length - 1}})" + else if Service.hasCustomPreferences() + 'custom-preferences' + else if !ShortFormApplicationService.showVeteransApplicationQuestion 'preferences-programs' else 'veterans-preference' diff --git a/app/assets/json/translations/react/en.json b/app/assets/json/translations/react/en.json index d124e4fe73..701d9fc35b 100644 --- a/app/assets/json/translations/react/en.json +++ b/app/assets/json/translations/react/en.json @@ -846,7 +846,7 @@ "listings.apply.submitAPaperApplication": "Submit a Paper Application", "listings.apply.visitAHousingCounselor": "Visit a local housing counselor for help with your application.", "listings.apply.visitHomeownershipSf": "Visit HomeownershipSF", - "listings.atTotalIncome": "at $%{income} %{per}", + "listings.atTotalIncome": "at $%{income} %{per}", "listings.availableAndWaitlist": "Available Units & Open Waitlist", "listings.availableUnits": "Available Units", "listings.availableUnitsAndOpenWaitlist": "Available Units & Open Waitlist", @@ -860,7 +860,7 @@ "listings.call": "Call %{phoneNumber}", "listings.cc&r": "Covenants, Conditions and Restrictions (CC&R's)", "listings.cc&rDescription": "The CC&R's explain the rules of the homeowners' association, and restrict how you can modify the property.", - "listings.clickForOtherLisitings": "Click Here for other rental and ownership affordable housing opportunities.", + "listings.clickForOtherLisitings": "Click Here for other rental and ownership affordable housing opportunities.", "listings.confirmedPreferenceList": "Confirmed %{preference} List", "listings.currentWaitlistSize": "Current Waitlist Size", "listings.customListingType.educator": "SF public schools employee housing", @@ -933,7 +933,7 @@ "listings.features.unitFeatures": "Unit Features", "listings.features.utilities": "Utilities", "listings.finalWaitlistSize": "Final Waitlist Size", - "listings.forHouseholdSize": "for %{size} %{people}", + "listings.forHouseholdSize": "for %{size} %{people}", "listings.forIncomeCalculations": "For income calculations, household size includes everyone (all ages) living in the unit.", "listings.habitat.applicationProcess.ol1": "You must go to a Habitat for Humanity information session.", "listings.habitat.applicationProcess.ol10": "You can move into your new home!", @@ -966,7 +966,7 @@ "listings.housingProgram": "Housing Program", "listings.importantProgramRules": "Important Program Rules", "listings.includesPriorityUnits": "Includes Priority Units for:", - "listings.includingChildren": "(including %{number} %{children} under 6)", + "listings.includingChildren": "(including %{number} %{children} under 6)", "listings.incomeExceptions.intro": "People in your household may need special income calculations if they:", "listings.incomeExceptions.nontaxable": "Receive non-taxable income (non-taxable income might include SSI, SSDI, child support payments, and worker’s compensation benefits).", "listings.incomeExceptions.students": "Are full-time students (but not the primary applicant).", diff --git a/app/assets/json/translations/react/es.json b/app/assets/json/translations/react/es.json index 61517774ad..4c48b47050 100644 --- a/app/assets/json/translations/react/es.json +++ b/app/assets/json/translations/react/es.json @@ -495,12 +495,12 @@ "f2ReviewTerms.title": "Términos", "footer.cityCountyOfSf": "Ciudad y condado de San Francisco", "footer.contact": "Comuníquese con", - "footer.dahliaDescription": "DAHLIA: El Portal de Vivienda de San Francisco es un proyecto de la
Oficina de Desarrollo Comunitario y de Viviendas del Alcalde", + "footer.dahliaDescription": "DAHLIA: El Portal de Vivienda de San Francisco es un proyecto de la
Oficina de Desarrollo Comunitario y de Viviendas del Alcalde", "footer.disclaimer": "Aviso de responsabilidad", "footer.forGeneralQuestions": "Si desea hacer una consulta general sobre el programa, comuníquese con MOHCD al 415-701-5500.", "footer.forListingQuestions": "Si desea hacer preguntas sobre los anuncios y la solicitud, comuníquese con el agente del anuncio.", "footer.giveFeedback": "Envíe sus comentarios", - "footer.inPartnershipWith": "en colaboración con la
Departamento de Servicios Digitales de San Francisco (San Francisco Digital Services)
Oficina de Innovación Cívica de la Alcaldía (Mayor's Office of Civic Innovation)", + "footer.inPartnershipWith": "en colaboración con la
Departamento de Servicios Digitales de San Francisco (San Francisco Digital Services)
Oficina de Innovación Cívica de la Alcaldía (Mayor's Office of Civic Innovation)", "footer.listingQuestions": "Si desea hacer preguntas sobre los anuncios y la solicitud, comuníquese con el agente del anuncio.", "footer.privacyPolicy": "Política de Privacidad", "forgotPassword.emailSentDescription": "Recibirá un correo electrónico con un enlace para restablecer su contraseña.", @@ -842,7 +842,7 @@ "listings.apply.submitAPaperApplication": "Presente una solicitud impresa", "listings.apply.visitAHousingCounselor": "Visite a un consejero de vivienda local si desea recibir ayuda con su solicitud.", "listings.apply.visitHomeownershipSf": "Visite HomeownershipSF", - "listings.atTotalIncome": "a $%{income} %{per}", + "listings.atTotalIncome": "a $%{income} %{per}", "listings.availableAndWaitlist": "Unidades disponibles y lista de espera abierta", "listings.availableUnits": "Unidades disponibles", "listings.availableUnitsAndOpenWaitlist": "Unidades disponibles y lista de espera abierta", @@ -856,7 +856,7 @@ "listings.call": "Llame al %{phoneNumber}", "listings.cc&r": "Cláusulas, condiciones y restricciones (Covenants, Conditions and Restrictions, CC&R)", "listings.cc&rDescription": "Las CC&R establecen las normas de la asociación de propietarios y limitan la modificación de la propiedad.", - "listings.clickForOtherLisitings": "Haga clic aquí para ver otras oportunidades de alquiler y de compra de vivienda asequible.", + "listings.clickForOtherLisitings": "Haga clic aquí para ver otras oportunidades de alquiler y de compra de vivienda asequible.", "listings.confirmedPreferenceList": "Lista de %{preference} confirmada", "listings.currentWaitlistSize": "Tamaño de la lista de espera actual", "listings.customListingType.educator": "Viviendas para empleados de las escuelas públicas de SF", @@ -929,7 +929,7 @@ "listings.features.unitFeatures": "Características de la unidad", "listings.features.utilities": "Servicios públicos", "listings.finalWaitlistSize": "Tamaño de la lista de espera final", - "listings.forHouseholdSize": "para %{size} %{people}", + "listings.forHouseholdSize": "para %{size} %{people}", "listings.forIncomeCalculations": "Para el cálculo de los ingresos, tenga en cuenta que el tamaño del grupo familiar incluye a todas las personas (de cualquier edad) que viven en la unidad.", "listings.habitat.applicationProcess.ol1": "Debe asistir a una sesión informativa de Habitat for Humanity.", "listings.habitat.applicationProcess.ol10": "¡Puede mudarse a su nueva casa!", @@ -962,7 +962,7 @@ "listings.housingProgram": "Programa de vivienda", "listings.importantProgramRules": "Reglas Importantes del Programa", "listings.includesPriorityUnits": "Incluye unidades prioritarias para:", - "listings.includingChildren": "(incluidos %{number} %{children} menores de 6 años)", + "listings.includingChildren": "(incluidos %{number} %{children} menores de 6 años)", "listings.incomeExceptions.intro": "Las personas en su hogar pueden necesitar cálculos de ingresos especiales en el caso que ellos", "listings.incomeExceptions.nontaxable": "Recibir ingresos no gravables (los ingresos no gravables pueden incluir SSI, SSDI, pagos de manutención infantil e indemnizaciones por accidentes laborales).", "listings.incomeExceptions.students": "Sean estudiantes de tiempo completo (no se aplica al solicitante principal)", diff --git a/app/assets/json/translations/react/tl.json b/app/assets/json/translations/react/tl.json index bc9a07cce1..5ed9ea9093 100644 --- a/app/assets/json/translations/react/tl.json +++ b/app/assets/json/translations/react/tl.json @@ -495,12 +495,12 @@ "f2ReviewTerms.title": "Mga Patakaran", "footer.cityCountyOfSf": "Lungsod at County ng San Francisco", "footer.contact": "Kontak", - "footer.dahliaDescription": "DAHLIA: Ang San Francisco Housing Portal ay proyekto ng
Opisina para sa Pabahay at Pagpapaunlad ng Komunidad (Housing and Community Development) ng Mayor ", + "footer.dahliaDescription": "DAHLIA: Ang San Francisco Housing Portal ay proyekto ng
Opisina para sa Pabahay at Pagpapaunlad ng Komunidad (Housing and Community Development) ng Mayor ", "footer.disclaimer": "Pagwawaksi ng Pananagutan (Disclaimer)", "footer.forGeneralQuestions": "Para sa mga tanong tungkol sa pangkalahatang programa, puwede ninyong tawagan ang MOHCD sa 415-701-5500.", "footer.forListingQuestions": "Para sa mga tanong tungkol sa nakalistang pabahay at aplikasyon, pakikontak ang property agent o Ahente para sa pagpapaupa na makikita sa NAKALISTANG PABAHAY.", "footer.giveFeedback": "Magbigay ng Opinyon ", - "footer.inPartnershipWith": "sa pakikipagtulungan sa
Serbisyong Pang-Digital ng San Francisco (San Francisco Digital Services)
Opisina ng Meyor sa Sibikang Inobasyon (Office of Civic Innovation)", + "footer.inPartnershipWith": "sa pakikipagtulungan sa
Serbisyong Pang-Digital ng San Francisco (San Francisco Digital Services)
Opisina ng Meyor sa Sibikang Inobasyon (Office of Civic Innovation)", "footer.listingQuestions": "Para sa mga tanong tungkol sa nakalistang pabahay at aplikasyon, pakikontak ang property agent o Ahente para sa pagpapaupa na makikita sa NAKALISTANG PABAHAY.", "footer.privacyPolicy": "Polisiya Ukol sa Pagiging Pribado ng Impormasyon ", "forgotPassword.emailSentDescription": "Makatatanggap kayo ng email na may link para mabago ninyo ang password. ", @@ -842,7 +842,7 @@ "listings.apply.submitAPaperApplication": "Isumite ang Papel na Aplikasyon", "listings.apply.visitAHousingCounselor": "Bumisita sa lokal na housing counselor (tagapayo para sa bahay) para matulungan kayo sa inyong aplikasyon.", "listings.apply.visitHomeownershipSf": "Bisitahin ang Homeownership SF", - "listings.atTotalIncome": "sa $%{income} %{per}", + "listings.atTotalIncome": "sa $%{income} %{per}", "listings.availableAndWaitlist": "Mga Makukuhang Unit at Bukas na Waitlist (listahan ng mga naghihintay magkaroon ng pabahay)", "listings.availableUnits": "Mga Makukuhang Unit ", "listings.availableUnitsAndOpenWaitlist": "Mga Makukuhang Unit at Bukas na Waitlist (listahan ng mga naghihintay magkaroon ng pabahay)", @@ -856,7 +856,7 @@ "listings.call": "Tumawag sa %{phoneNumber}", "listings.cc&r": "Mga Kasunduan, Kondisyon, at Restriksyon (Covenants, Conditions and Restrictions, CC&R's)", "listings.cc&rDescription": "Ipinaliliwanag ng CC&Rs ang mga patakaran ng samahan ng mga may-ari ng bahay o homeowner's association, at ang mga restriksiyon sa pagbabagong magagawa ninyo sa ari-arian. ", - "listings.clickForOtherLisitings": "Mag-klik Dito para sa iba pang oportunidad sa pag-upa at sa pagmamay-ari ng abot-kayang pabahay. ", + "listings.clickForOtherLisitings": "Mag-klik Dito para sa iba pang oportunidad sa pag-upa at sa pagmamay-ari ng abot-kayang pabahay. ", "listings.confirmedPreferenceList": "Kumpirmadong Listahan para sa %{preference} ", "listings.currentWaitlistSize": "Kasalukuyang Laki ng Waitlist (listahan ng mga naghihintay magkaroon ng pabahay)", "listings.customListingType.educator": "Pabahay para sa empleyado ng mga pampublikong paaralan ng SF", @@ -929,7 +929,7 @@ "listings.features.unitFeatures": "Mga Katangian ng Unit", "listings.features.utilities": "Mga Utility", "listings.finalWaitlistSize": "Pinal na Laki ng Waitlist (listahan ng mga naghihintay magkaroon ng pabahay).", - "listings.forHouseholdSize": "para sa %{size} %{people}", + "listings.forHouseholdSize": "para sa %{size} %{people}", "listings.forIncomeCalculations": "Para sa mga kalkulasyon ng kita, kasama sa laki ng kabahayan (household size) ang lahat ng tao (lahat ng edad) na nakatira sa unit.", "listings.habitat.applicationProcess.ol1": "Dapat kang dumalo sa isang sesyon para sa pagbibigay ng impormasyon ng Habitat for Humanity", "listings.habitat.applicationProcess.ol10": "Puwede kang lumipat sa iyong bagong bahay!", @@ -962,7 +962,7 @@ "listings.housingProgram": "Programa sa Pabahay (Housing Program)", "listings.importantProgramRules": "Mahahalagang Patakaran ng Programa ", "listings.includesPriorityUnits": "Kasama ang mga Priority Unit para sa:", - "listings.includingChildren": "(kasama ang %{number} %{children} mas bata sa edad na 6)", + "listings.includingChildren": "(kasama ang %{number} %{children} mas bata sa edad na 6)", "listings.incomeExceptions.intro": "Posibleng kailangan ng mga tao sa inyong kabahayan ng espesyal na pagkakalkula ng kita kung sila ay:", "listings.incomeExceptions.nontaxable": "Makatanggap ng di nabubuwisang kita (maaaring kasama sa di nabubuwisang kita ang SSI, SSDI, mga pagbabayad ng sustento sa bata, at mga benepisyo ng kabayaran ng manggagawa).", "listings.incomeExceptions.students": "Mga buong oras na estudyante (pero hindi ang pangunahing aplikante)", diff --git a/app/assets/json/translations/react/zh.json b/app/assets/json/translations/react/zh.json index 69a2b04c53..cc8dd60340 100644 --- a/app/assets/json/translations/react/zh.json +++ b/app/assets/json/translations/react/zh.json @@ -495,12 +495,12 @@ "f2ReviewTerms.title": "條款", "footer.cityCountyOfSf": "舊金山市和郡", "footer.contact": "聯絡資訊", - "footer.dahliaDescription": "可負擔房屋項目名單、資訊和申請表資料庫 (Database of Affordable Housing Listings, Information, and Applications, DAHLIA):三藩市房屋網站 (San Francisco Housing Portal) 是
市長住房與社區發展辦公室的一項專案", + "footer.dahliaDescription": "可負擔房屋項目名單、資訊和申請表資料庫 (Database of Affordable Housing Listings, Information, and Applications, DAHLIA):三藩市房屋網站 (San Francisco Housing Portal) 是
市長住房與社區發展辦公室的一項專案", "footer.disclaimer": "免責聲明", "footer.forGeneralQuestions": "有關一般計畫詢問,可以致電 MOHCD,電話是 415-701-5500。", "footer.forListingQuestions": "如需瞭解有關房屋項目和申請方面的問題,請與名單中例出的機構聯絡。", "footer.giveFeedback": "提供意見", - "footer.inPartnershipWith": "本專案與三藩市數碼服務部和 (San Francisco Digital Services)
市長市政創新辦公室 (Mayor's Office of Civic Innovation) 合作。", + "footer.inPartnershipWith": "本專案與三藩市數碼服務部和 (San Francisco Digital Services)
市長市政創新辦公室 (Mayor's Office of Civic Innovation) 合作。", "footer.listingQuestions": "如需瞭解有關房屋項目和申請方面的問題,請與名單中例出的機構聯絡。", "footer.privacyPolicy": "隱私政策", "forgotPassword.emailSentDescription": "您會收到一封電子郵件,裡面包含重設密碼的連結。", @@ -842,7 +842,7 @@ "listings.apply.submitAPaperApplication": "遞交書面申請表", "listings.apply.visitAHousingCounselor": "約見本地房屋顧問以尋求申請協助。", "listings.apply.visitHomeownershipSf": "點擊 HomeownershipSF", - "listings.atTotalIncome": "總收入:$%{income} %{per}", + "listings.atTotalIncome": "總收入:$%{income} %{per}", "listings.availableAndWaitlist": "現有單位與開放等候名單", "listings.availableUnits": "可用單位", "listings.availableUnitsAndOpenWaitlist": "現有單位與開放等候名單", @@ -856,7 +856,7 @@ "listings.call": "電洽 %{phoneNumber}", "listings.cc&r": "契約、條件與限制 (Covenants, Conditions and Restrictions, CC&R)", "listings.cc&rDescription": "CC&R 說明有關屋主協會的規定,並且限制您如何裝修該物業。", - "listings.clickForOtherLisitings": "按此查看其他租賃和購買平價住房機會。", + "listings.clickForOtherLisitings": "按此查看其他租賃和購買平價住房機會。", "listings.confirmedPreferenceList": "確認的 %{preference} 名單", "listings.currentWaitlistSize": "目前等候名單中的人數", "listings.customListingType.educator": "三藩市公共學校雇員住房", @@ -929,7 +929,7 @@ "listings.features.unitFeatures": "單位特色", "listings.features.utilities": "水電瓦斯費", "listings.finalWaitlistSize": "最終等候名單中的人數", - "listings.forHouseholdSize": "人數:%{size} %{people}", + "listings.forHouseholdSize": "人數:%{size} %{people}", "listings.forIncomeCalculations": "為計算收入,家庭人數須包含居住在該單位的所有成員 (無論年齡)。", "listings.habitat.applicationProcess.ol1": "您必須參加 Habitat for Humanity 說明會。", "listings.habitat.applicationProcess.ol10": "您可以搬入新家!", @@ -962,7 +962,7 @@ "listings.housingProgram": "住房計劃", "listings.importantProgramRules": "重要計畫規定", "listings.includesPriorityUnits": "包括以下各項的優先住房單位:", - "listings.includingChildren": "(包括%{number} %{children}6歲以下兒童)", + "listings.includingChildren": "(包括%{number} %{children}6歲以下兒童)", "listings.incomeExceptions.intro": "如果您家中的成員符合以下情況,可能需要特別收入計算方式:", "listings.incomeExceptions.nontaxable": "獲得非應稅收入 (非應稅收入可能包括 SSI、SSDI、兒童支援費用和勞工賠償權益)。", "listings.incomeExceptions.students": "是全日制學生(但不是主申請人)", diff --git a/app/javascript/__tests__/api/listingsApiService.test.ts b/app/javascript/__tests__/api/listingsApiService.test.ts index 95f2b42f19..17d373365e 100644 --- a/app/javascript/__tests__/api/listingsApiService.test.ts +++ b/app/javascript/__tests__/api/listingsApiService.test.ts @@ -23,7 +23,7 @@ describe("listingsApiService", () => { income_total: 70000, include_children_under_6: true, children_under_6: "2", - type: null, + type: "", } expect(getEligibilityQueryString(filters, "rental")).toEqual( "householdsize=4&incomelevel=70000&includeChildrenUnder6=true&childrenUnder6=2&listingsType=rental" @@ -35,8 +35,8 @@ describe("listingsApiService", () => { income_timeframe: "per_year", income_total: 70000, include_children_under_6: false, - children_under_6: null, - type: null, + children_under_6: "", + type: "", } expect(getEligibilityQueryString(filters, "rental")).toEqual( "householdsize=4&incomelevel=70000&includeChildrenUnder6=false&childrenUnder6=&listingsType=rental" @@ -49,7 +49,7 @@ describe("listingsApiService", () => { income_total: 5000, include_children_under_6: true, children_under_6: "4", - type: null, + type: "", } expect(getEligibilityQueryString(filters, "rental")).toContain("60000") }) @@ -60,7 +60,7 @@ describe("listingsApiService", () => { income_total: 5000, include_children_under_6: true, children_under_6: "4", - type: null, + type: "", } expect(getEligibilityQueryString(filters, "ownership")).toContain("&listingsType=ownership") }) diff --git a/app/javascript/__tests__/components/ErrorBoundary.test.tsx b/app/javascript/__tests__/components/ErrorBoundary.test.tsx index 65a2e7aa29..c4e4705086 100644 --- a/app/javascript/__tests__/components/ErrorBoundary.test.tsx +++ b/app/javascript/__tests__/components/ErrorBoundary.test.tsx @@ -31,7 +31,7 @@ describe("ErrorBoundary", () => { console.debug.mockRestore() }) - it("displays content when there is no error", async (done) => { + it("displays content when there is no error", async () => { const content = "test 123" const { getByText } = await renderAndLoadAsync( @@ -40,10 +40,9 @@ describe("ErrorBoundary", () => { ) getByText(content) - done() }) - it("display fallback UI when error thrown for content level error boundary", async (done) => { + it("display fallback UI when error thrown for content level error boundary", async () => { const { getByText } = await renderAndLoadAsync(
@@ -53,10 +52,9 @@ describe("ErrorBoundary", () => { ) getByText("An error occurred. Check back later.") - done() }) - it("throws error when error occurs for page level boundary", async (done) => { + it("throws error when error occurs for page level boundary", async () => { // In development environments, errors bubble up to window object so checking for error instead of redirect. // https://reactjs.org/docs/react-component.html#componentdidcatch @@ -69,6 +67,5 @@ describe("ErrorBoundary", () => { ) ).rejects.toThrow(errorMsg) - done() }) }) diff --git a/app/javascript/__tests__/components/TextTruncate.test.tsx b/app/javascript/__tests__/components/TextTruncate.test.tsx index 1a034cc284..c034dae8d4 100644 --- a/app/javascript/__tests__/components/TextTruncate.test.tsx +++ b/app/javascript/__tests__/components/TextTruncate.test.tsx @@ -3,13 +3,12 @@ import { TextTruncate } from "../../components/TextTruncate" import { renderAndLoadAsync } from "../__util__/renderUtils" describe("TextTruncate", () => { - it("truncates a string and adds an ellipsis when longer than 400 characters", async (done) => { + it("truncates a string and adds an ellipsis when longer than 400 characters", async () => { const content = "

As of 01/04/2022, there are 5 applications pending review for the remaining 2-Bedroom BMR unit. The application period began on 05/03/2021 at 8:00 AM PT. If interested, please submit an electronic application via the ShareFile secure link.



Instructions: Compile the application form and all required documents into one PDF file, and name the PDF file “MIRASF – Last Name, First Name” (Example, 123 Sample Street Unit A – Smith, John). Use the divider pages and place the corresponding documentation behind each divider page. Applications received in multiple files, an incorrect order, formats other than PDF or without supporting documents will not be accepted or reviewed. If you do not have internet access or are unable to submit electronically, please contact a housing counselor for assistance.



For the First Come First Served available unit details including sales price and HOA dues, please view the MIRA BMR Unit Matrix.

" const { queryByText } = await renderAndLoadAsync() // Check that there is no ellipsis expect(queryByText(/\.\.\./)).toBeNull() - done() }) }) diff --git a/app/javascript/__tests__/jest.setup.js b/app/javascript/__tests__/jest.setup.js new file mode 100644 index 0000000000..bdf9d5e6da --- /dev/null +++ b/app/javascript/__tests__/jest.setup.js @@ -0,0 +1,5 @@ +// polyfill for TextEncoder +// https://stackoverflow.com/questions/68468203/why-am-i-getting-textencoder-is-not-defined-in-jest +global.TextEncoder = require("util").TextEncoder + +HTMLCanvasElement.prototype.getContext = jest.fn() diff --git a/app/javascript/__tests__/layouts/AssistanceLayout.test.tsx b/app/javascript/__tests__/layouts/AssistanceLayout.test.tsx index fe7f363504..593869ece8 100644 --- a/app/javascript/__tests__/layouts/AssistanceLayout.test.tsx +++ b/app/javascript/__tests__/layouts/AssistanceLayout.test.tsx @@ -13,7 +13,7 @@ jest.mock("react-helmet-async", () => { }) describe("", () => { - it("renders children", async (done) => { + it("renders children", async () => { const { getByTestId } = await renderAndLoadAsync(

{CHILD_CONTENT}

@@ -22,10 +22,9 @@ describe("", () => { const mainContent = getByTestId("assistance-main-content") expect(within(mainContent).getByText(CHILD_CONTENT)).not.toBeNull() - done() }) - it("renders PageHeader", async (done) => { + it("renders PageHeader", async () => { const TitleText = "Title Text" const SubtitleText = "SubTitle Text" const { getAllByText } = await renderAndLoadAsync( @@ -36,7 +35,6 @@ describe("", () => { expect(getAllByText(TitleText).length).not.toBeNull() expect(getAllByText(SubtitleText).length).not.toBeNull() - done() }) describe("Contact Bar", () => { diff --git a/app/javascript/__tests__/layouts/MetaTags.test.tsx b/app/javascript/__tests__/layouts/MetaTags.test.tsx index f181b9cea6..7baaed81ec 100644 --- a/app/javascript/__tests__/layouts/MetaTags.test.tsx +++ b/app/javascript/__tests__/layouts/MetaTags.test.tsx @@ -22,8 +22,7 @@ describe("", () => { it("renders a description", () => { const { container } = render() - expect(container.querySelector("meta[name='description']").getAttribute("content")).toEqual( - "The Description" - ) + const descriptionMeta = container.querySelector("meta[name='description']") + expect(descriptionMeta?.getAttribute("content")).toEqual("The Description") }) }) diff --git a/app/javascript/__tests__/layouts/__snapshots__/AssistanceLayout.test.tsx.snap b/app/javascript/__tests__/layouts/__snapshots__/AssistanceLayout.test.tsx.snap index 88b730b494..e7c6701d45 100644 --- a/app/javascript/__tests__/layouts/__snapshots__/AssistanceLayout.test.tsx.snap +++ b/app/javascript/__tests__/layouts/__snapshots__/AssistanceLayout.test.tsx.snap @@ -8,7 +8,9 @@ exports[` Contact Bar renders Contact Information 1`] = `
- + <title + class="notranslate" + > Title Text { jest.resetAllMocks() }) - it("displays listing details eligibility section and no Building Selection Criteria Link", async (done) => { + it("displays listing details eligibility section and no Building Selection Criteria Link", async () => { const testListing = { ...closedRentalListing, Building_Selection_Criteria: "", @@ -54,7 +54,7 @@ describe("ListingDetailsEligibility", () => { axios.get.mockResolvedValue({ data: { preferences: defaultPreferences } }) - const { asFragment, findByText } = render( + const { asFragment, findByText } = await renderAndLoadAsync( { ) - expect(await findByText("Eligibility")).toBeDefined() + await waitFor(() => { + expect(findByText("Eligibility")).toBeDefined() + }) expect(asFragment()).toMatchSnapshot() - done() }) - it("displays listing details eligibility section for a sales listing", async (done) => { + it("displays listing details eligibility section for a sales listing", async () => { axios.get.mockResolvedValue({ data: { preferences: defaultPreferences } }) - const { asFragment, findByText } = render( + const { asFragment, findByText } = await renderAndLoadAsync( { ) - expect(await findByText("Eligibility")).toBeDefined() + await waitFor(() => { + expect(findByText("Eligibility")).toBeDefined() + }) expect(asFragment()).toMatchSnapshot() - done() }) - it("displays listing details eligibility section for a listing with only SRO units", async (done) => { + it("displays listing details eligibility section for a listing with only SRO units", async () => { axios.get.mockResolvedValue({ data: { preferences: defaultPreferences } }) - const { asFragment, findByText } = render( + const { asFragment, findByText } = await renderAndLoadAsync( { ) - expect(await findByText("Eligibility")).toBeDefined() + await waitFor(() => { + expect(findByText("Eligibility")).toBeDefined() + }) expect(asFragment()).toMatchSnapshot() - done() }) - it("displays listing details eligibility section for an SRO listing with expanded occupancy units", async (done) => { + it("displays listing details eligibility section for an SRO listing with expanded occupancy units", async () => { const listing = { ...sroRentalListing, Id: "a0W0P00000FIuv3UAD" } axios.get.mockResolvedValue({ data: { preferences: defaultPreferences } }) - const { asFragment, findByText } = render( + const { asFragment, findByText } = await renderAndLoadAsync( { ) - expect(await findByText("Eligibility")).toBeDefined() + await waitFor(() => { + expect(findByText("Eligibility")).toBeDefined() + }) expect(asFragment()).toMatchSnapshot() - done() }) - it("displays listing details eligibility section for an SRO listing with a mix of SRO units and non-SRO units", async (done) => { + it("displays listing details eligibility section for an SRO listing with a mix of SRO units and non-SRO units", async () => { axios.get.mockResolvedValue({ data: { preferences: defaultPreferences } }) - const { asFragment, findByText } = render( + const { asFragment, findByText } = await renderAndLoadAsync( { /> ) - expect(await findByText("Eligibility")).toBeDefined() + + await waitFor(() => { + expect(findByText("Eligibility")).toBeDefined() + }) expect(asFragment()).toMatchSnapshot() - done() }) - it("displays listing details eligibility section when habitat listing", async (done) => { + it("displays listing details eligibility section when habitat listing", async () => { axios.get.mockResolvedValue({ data: { preferences: defaultPreferences } }) - const { asFragment, findByText } = render( + const { asFragment, findByText } = await renderAndLoadAsync( { ) - expect(await findByText("Eligibility")).toBeDefined() + await waitFor(() => { + expect(findByText("Eligibility")).toBeDefined() + }) expect(asFragment()).toMatchSnapshot() - done() }) - it("displays custom listing details check if you're eligible section for Shirley Chisholm listing 1", async (done) => { + it("displays custom listing details check if you're eligible section for Shirley Chisholm listing 1", async () => { axios.get.mockResolvedValue({ data: { preferences: defaultPreferences } }) const { asFragment, findByText } = render( @@ -233,10 +240,9 @@ describe("ListingDetailsEligibility", () => { await findByText(t("listings.customListingType.educator.eligibility.part1")) ).toBeDefined() expect(asFragment()).toMatchSnapshot() - done() }) - it("displays custom listing details check if you're eligible section for Shirley Chisholm listing 2", async (done) => { + it("displays custom listing details check if you're eligible section for Shirley Chisholm listing 2", async () => { axios.get.mockResolvedValue({ data: { preferences: defaultPreferences } }) const { asFragment, findByText } = render( @@ -263,10 +269,9 @@ describe("ListingDetailsEligibility", () => { await findByText(t("listings.customListingType.educator.eligibility.priority")) ).toBeDefined() expect(asFragment()).toMatchSnapshot() - done() }) - it("displays ListingDetailsChisholmPreferences for educator listing 1", async (done) => { + it("displays ListingDetailsChisholmPreferences for educator listing 1", async () => { axios.get.mockResolvedValue({ data: { listings: [rentalEducatorListing1] } }) const { asFragment, findByText } = await renderAndLoadAsync( { expect(findByText(t("listings.customListingType.educator.preferences.part1"))).toBeDefined() expect(asFragment()).toMatchSnapshot() - done() }) - it("does not display ListingDetailsChisholmPreferences for a non-Chisholm listing", async (done) => { + it("does not display ListingDetailsChisholmPreferences for a non-Chisholm listing", async () => { axios.get.mockResolvedValue({ data: { listings: [sroRentalListing] } }) const { asFragment, findByText } = await renderAndLoadAsync( { expect(findByText(t("listings.customListingType.educator.preferences.part1"))).not.toBeNull() expect(asFragment()).toMatchSnapshot() - done() }) }) diff --git a/app/javascript/__tests__/modules/listingDetails/ListingDetailsFeatures.test.tsx b/app/javascript/__tests__/modules/listingDetails/ListingDetailsFeatures.test.tsx index 034642827b..1b6e6f1a37 100644 --- a/app/javascript/__tests__/modules/listingDetails/ListingDetailsFeatures.test.tsx +++ b/app/javascript/__tests__/modules/listingDetails/ListingDetailsFeatures.test.tsx @@ -18,7 +18,7 @@ describe("ListingDetailsFeatures", () => { jest.resetAllMocks() }) - it("displays listing details features section when rental listing", async (done) => { + it("displays listing details features section when rental listing", async () => { // This component pulls in react-media, which needs this custom mock window.matchMedia = jest.fn().mockImplementation((query) => { return { @@ -52,10 +52,9 @@ describe("ListingDetailsFeatures", () => { expect(await findAllByTestId("content-accordion-button")).toHaveLength(3) expect(asFragment()).toMatchSnapshot() - done() }) - it("displays listing details features section when sales listing", async (done) => { + it("displays listing details features section when sales listing", async () => { axios.get.mockResolvedValue({ data: { units: openSaleListing.Units, preferences: defaultPreferences }, }) @@ -93,10 +92,9 @@ describe("ListingDetailsFeatures", () => { expect(await findAllByTestId("content-accordion-button")).toHaveLength(3) expect(asFragment()).toMatchSnapshot() - done() }) - it("displays a pdf hyperlink when Pricing_Matrix url exists in sales listing", async (done) => { + it("displays a pdf hyperlink when Pricing_Matrix url exists in sales listing", async () => { axios.get.mockResolvedValue({ data: { units: openSaleListing.Units, preferences: defaultPreferences }, }) @@ -137,6 +135,5 @@ describe("ListingDetailsFeatures", () => { expect(await findAllByTestId("content-accordion-button")).toHaveLength(3) expect(asFragment()).toMatchSnapshot() - done() }) }) diff --git a/app/javascript/__tests__/modules/listingDetails/ListingDetailsPreferences.test.tsx b/app/javascript/__tests__/modules/listingDetails/ListingDetailsPreferences.test.tsx index acab780374..04f9c4aff3 100644 --- a/app/javascript/__tests__/modules/listingDetails/ListingDetailsPreferences.test.tsx +++ b/app/javascript/__tests__/modules/listingDetails/ListingDetailsPreferences.test.tsx @@ -1,9 +1,10 @@ import React from "react" -import { render, cleanup } from "@testing-library/react" +import { cleanup, waitFor } from "@testing-library/react" import { ListingDetailsPreferences } from "../../../modules/listingDetails/ListingDetailsPreferences" import { preferences as defaultPreferences } from "../../data/RailsListingPreferences/lottery-preferences-default" import { preferences as sixPreferences } from "../../data/RailsListingPreferences/lottery-preferences-six" +import { renderAndLoadAsync } from "../../__util__/renderUtils" const axios = require("axios") @@ -16,21 +17,25 @@ describe("ListingDetailsPreferences", () => { jest.resetAllMocks() }) - it("display 3 default preferences - COP, DTHP, L/W", (done) => { + it("display 3 default preferences - COP, DTHP, L/W", async () => { axios.get.mockResolvedValue({ data: { preferences: defaultPreferences } }) - const { asFragment } = render() + const { asFragment, getByText } = await renderAndLoadAsync( + + ) + await waitFor(() => getByText("Certificate of Preference (COP)")) expect(asFragment()).toMatchSnapshot() - done() }) - it("display 6 preferences", (done) => { + it("display 6 preferences", async () => { axios.get.mockResolvedValue({ data: { preferences: sixPreferences } }) - const { asFragment } = render() + const { asFragment, getByText } = await renderAndLoadAsync( + + ) + await waitFor(() => getByText("Certificate of Preference (COP)")) expect(asFragment()).toMatchSnapshot() - done() }) }) diff --git a/app/javascript/__tests__/modules/listingDetails/ListingDetailsUnitAccordions.test.tsx b/app/javascript/__tests__/modules/listingDetails/ListingDetailsUnitAccordions.test.tsx index 41266824af..c0fe6fb448 100644 --- a/app/javascript/__tests__/modules/listingDetails/ListingDetailsUnitAccordions.test.tsx +++ b/app/javascript/__tests__/modules/listingDetails/ListingDetailsUnitAccordions.test.tsx @@ -18,7 +18,7 @@ describe("ListingDetailsUnitAccordion", () => { jest.resetAllMocks() }) - it("displays the unit accordions for a given listing", async (done) => { + it("displays the unit accordions for a given listing", async () => { jest.setTimeout(30_000) axios.get.mockResolvedValue({ data: { listings: [], units: openSaleListing.Units } }) @@ -31,8 +31,8 @@ describe("ListingDetailsUnitAccordion", () => { fetchedUnits: true, fetchingAmiCharts: false, fetchedAmiCharts: false, - fetchingAmiChartsError: null, - fetchingUnitsError: null, + fetchingAmiChartsError: undefined, + fetchingUnitsError: undefined, }} > @@ -42,7 +42,6 @@ describe("ListingDetailsUnitAccordion", () => { expect(await findAllByTestId("content-accordion-button")).toHaveLength(3) expect(asFragment()).toMatchSnapshot() - done() }) it("displays spinner if no units and not fetching units", () => { @@ -55,8 +54,8 @@ describe("ListingDetailsUnitAccordion", () => { fetchedUnits: false, fetchingAmiCharts: false, fetchedAmiCharts: false, - fetchingAmiChartsError: null, - fetchingUnitsError: null, + fetchingAmiChartsError: undefined, + fetchingUnitsError: undefined, }} > @@ -76,8 +75,8 @@ describe("ListingDetailsUnitAccordion", () => { fetchedUnits: false, fetchingAmiCharts: false, fetchedAmiCharts: false, - fetchingAmiChartsError: null, - fetchingUnitsError: null, + fetchingAmiChartsError: undefined, + fetchingUnitsError: undefined, }} > diff --git a/app/javascript/__tests__/modules/listingDetails/__snapshots__/ListingDetailsFeatures.test.tsx.snap b/app/javascript/__tests__/modules/listingDetails/__snapshots__/ListingDetailsFeatures.test.tsx.snap index 9f855a8cca..e932372d2f 100644 --- a/app/javascript/__tests__/modules/listingDetails/__snapshots__/ListingDetailsFeatures.test.tsx.snap +++ b/app/javascript/__tests__/modules/listingDetails/__snapshots__/ListingDetailsFeatures.test.tsx.snap @@ -570,25 +570,25 @@ exports[`ListingDetailsFeatures displays listing details features section when r
-

-

- Tenants pay for gas, electricity. + > +

+ Tenants pay for gas, electricity. -
-
- For pet fees: Cat is allowed with a $500 refundable deposit, $250 non-refundable cleaning fee and a pet addendum. -
-
-
- Dogs are not allowed in the building. -
-
-
- One parking space per unit available for $175 a month. -

-

+
+
+ For pet fees: Cat is allowed with a $500 refundable deposit, $250 non-refundable cleaning fee and a pet addendum. +
+
+
+ Dogs are not allowed in the building. +
+
+
+ One parking space per unit available for $175 a month. +

+
diff --git a/app/javascript/__tests__/modules/listingDetails/__snapshots__/ListingDetailsHabitat.test.tsx.snap b/app/javascript/__tests__/modules/listingDetails/__snapshots__/ListingDetailsHabitat.test.tsx.snap index ce3059ac0a..5b2ceec77d 100644 --- a/app/javascript/__tests__/modules/listingDetails/__snapshots__/ListingDetailsHabitat.test.tsx.snap +++ b/app/javascript/__tests__/modules/listingDetails/__snapshots__/ListingDetailsHabitat.test.tsx.snap @@ -31,18 +31,18 @@ exports[`ListingDetailsHabitat displays habitat info when habitat listing 1`] = > Application Process -

-

- These units are offered by - - Habitat for Humanity Greater San Francisco - - in partnership with MOHCD -

-

+ +

+ These units are offered by + + Habitat for Humanity Greater San Francisco + + in partnership with MOHCD +

+
    @@ -99,20 +99,20 @@ exports[`ListingDetailsHabitat displays habitat info when habitat listing 1`] = > For income calculations, household size includes everyone (all ages) living in the unit.

    -

    -

    - People in your household may need - - special income calculations - - if they: -

    -

    + > +

    + People in your household may need + + special income calculations + + if they: +

    +
      diff --git a/app/javascript/__tests__/modules/listingDetails/__snapshots__/ListingDetailsPreferences.test.tsx.snap b/app/javascript/__tests__/modules/listingDetails/__snapshots__/ListingDetailsPreferences.test.tsx.snap index 9cc400824e..41a21eec33 100644 --- a/app/javascript/__tests__/modules/listingDetails/__snapshots__/ListingDetailsPreferences.test.tsx.snap +++ b/app/javascript/__tests__/modules/listingDetails/__snapshots__/ListingDetailsPreferences.test.tsx.snap @@ -2,54 +2,409 @@ exports[`ListingDetailsPreferences display 3 default preferences - COP, DTHP, L/W 1`] = ` -
      - - - - - -
      +
    • +
      + 1 + + st + +
      +

      + Certificate of Preference (COP) +

      +
      + Up to 6 units available +
      +
      + For households in which at least one member holds a Certificate of Preference from the former San Francisco Redevelopment Agency. COP holders were displaced by Agency action generally during the 1960s and 1970s. +
      + +
    • +
    • +
      + 2 + + nd + +
      +

      + Displaced Tenant Housing Preference (DTHP) +

      +
      + Up to 1 units available +
      +
      + For households in which at least one member holds a Displaced Tenant Housing Preference Certificate. DTHP Certificate holders are tenants who were evicted through either an Ellis Act Eviction or an Owner Move In Eviction, have been displaced by a fire, or who will experience an unaffordable rent increase due to affordability restrictions expiring. Once all units reserved for this preference are filled, remaining DTHP holders will receive Live/Work preference, regardless of their current residence or work location. +
      + +
    • +
    • +
      + 3 + + rd + +
      +

      + Live or Work in San Francisco Preference +

      +
      + Up to 6 units available +
      +
      + This is a custom description - For households in which at least one member lives or works in San Francisco. Requires submission of proof. Please note in order to claim Work Preference, the applicant currently work in San Francisco at least 75% of their working hours. +
      + +
    • +
`; exports[`ListingDetailsPreferences display 6 preferences 1`] = ` -
- - - - - -
+
  • +
    + 1 + + st + +
    +

    + Certificate of Preference (COP) +

    +
    + Up to 1 units available +
    +
    + For households in which at least one member holds a Certificate of Preference from the former San Francisco Redevelopment Agency. COP holders were displaced by Agency action generally during the 1960s and 1970s. +
    + +
  • +
  • +
    + 2 + + nd + +
    +

    + Rent Burdened / Assisted Housing Preference +

    +
    + Up to 2 units available +
    +
    + For households who are currently paying more than 50% of income for housing costs or are living in public housing or project based Section 8 housing within San Francisco. +
    + +
  • +
  • +
    + 3 + + rd + +
    +

    + Displaced Tenant Housing Preference (DTHP) +

    +
    + Up to 3 units available +
    +
    + For households in which at least one member holds a Displaced Tenant Housing Preference Certificate. DTHP Certificate holders are tenants who were evicted through either an Ellis Act Eviction or an Owner Move In Eviction, have been displaced by a fire, or who will experience an unaffordable rent increase due to affordability restrictions expiring. Once all units reserved for this preference are filled, remaining DTHP holders will receive Live/Work preference, regardless of their current residence or work location. +
    + +
  • +
  • +
    + 4 + + th + +
    +

    + Neighborhood Resident Housing Preference (NRHP) +

    +
    + Up to 4 units available +
    +
    + For households in which at least one member either lives within the Supervisor district of the project or within a half-mile of the project. Requires submission of proof of address. +
    + +
  • +
  • +
    + 5 + + th + +
    +

    + Live or Work in San Francisco Preference +

    +
    + Up to 5 units available +
    +
    + For households in which at least one member lives or works in San Francisco. Requires submission of proof. Please note in order to claim Work Preference, the applicant must currently work in San Francisco at least 75% of their working hours. +
    + +
  • +
  • +
    + 6 + + th + +
    +

    + Alice Griffith Housing Development Resident +

    +
    + Right to Return - Alice Griffith. For households in which at least one member is a former or current resident of Alice Griffith public housing. Please note, these units are not subsidized — the rent will not change if your income changes. +
    + +
  • +
    `; diff --git a/app/javascript/__tests__/modules/listingDetailsLottery/ListingDetailsLottery.test.tsx b/app/javascript/__tests__/modules/listingDetailsLottery/ListingDetailsLottery.test.tsx index ba16417242..c13a9896b5 100644 --- a/app/javascript/__tests__/modules/listingDetailsLottery/ListingDetailsLottery.test.tsx +++ b/app/javascript/__tests__/modules/listingDetailsLottery/ListingDetailsLottery.test.tsx @@ -5,13 +5,12 @@ import { screen } from "@testing-library/react" import { MobileListingDetailsLottery } from "../../../modules/listingDetailsLottery/MobileListingDetailsLottery" describe("ListingDetailsLottery", () => { - it("does not display if lottery is not complete", async (done) => { + it("does not display if lottery is not complete", async () => { await renderAndLoadAsync( ) const subheader = screen.queryByText("Lottery selection, important dates and contact") expect(subheader).not.toBeInTheDocument() - done() }) }) diff --git a/app/javascript/__tests__/modules/listingDetailsLottery/ListingDetailsLotteryResults.test.tsx b/app/javascript/__tests__/modules/listingDetailsLottery/ListingDetailsLotteryResults.test.tsx index 1a0d357261..b478182214 100644 --- a/app/javascript/__tests__/modules/listingDetailsLottery/ListingDetailsLotteryResults.test.tsx +++ b/app/javascript/__tests__/modules/listingDetailsLottery/ListingDetailsLotteryResults.test.tsx @@ -12,15 +12,14 @@ const axios = require("axios") jest.mock("axios") describe("ListingDetailsLotteryResults", () => { - it("does not display if lottery is not complete", async (done) => { + it("does not display if lottery is not complete", async () => { await renderAndLoadAsync() const viewButton = screen.queryByText("View Lottery Results") expect(viewButton).not.toBeInTheDocument() - done() }) - it("displays if lottery is complete", async (done) => { + it("displays if lottery is complete", async () => { axios.get.mockResolvedValue({ data: lotteryResultRentalOne }) const { asFragment, findByText } = render( @@ -29,10 +28,9 @@ describe("ListingDetailsLotteryResults", () => { await findByText("View Lottery Results") expect(asFragment()).toMatchSnapshot() - done() }) - it("displays with summary if lottery is complete", async (done) => { + it("displays with summary if lottery is complete", async () => { axios.get.mockResolvedValue({ data: lotteryResultRentalOne }) const { asFragment, findByText } = render( @@ -42,6 +40,5 @@ describe("ListingDetailsLotteryResults", () => { await findByText("View Lottery Results") expect(asFragment()).toMatchSnapshot() - done() }) }) diff --git a/app/javascript/__tests__/modules/listingDetailsLottery/ListingDetailsLotterySearchForm.test.tsx b/app/javascript/__tests__/modules/listingDetailsLottery/ListingDetailsLotterySearchForm.test.tsx index 42a5e93b35..e0631eec59 100644 --- a/app/javascript/__tests__/modules/listingDetailsLottery/ListingDetailsLotterySearchForm.test.tsx +++ b/app/javascript/__tests__/modules/listingDetailsLottery/ListingDetailsLotterySearchForm.test.tsx @@ -3,7 +3,7 @@ import { ListingDetailsLotterySearchForm } from "../../../modules/listingDetails import { lotteryCompleteRentalListing } from "../../data/RailsRentalListing/listing-rental-lottery-complete" import { lotteryResultRentalThree } from "../../data/RailsLotteryResult/lottery-result-rental-three" import userEvent from "@testing-library/user-event" -import { render, cleanup, act } from "@testing-library/react" +import { render, cleanup } from "@testing-library/react" import { lotteryResultRentalInvalidLotteryNumber } from "../../data/RailsLotteryResult/lottery-result-rental-invalid-lottery-number" import { renderAndLoadAsync } from "../../__util__/renderUtils" @@ -18,7 +18,7 @@ describe("ListingDetailsLotteryModal", () => { jest.resetAllMocks() }) - it("displays initial view with form and listing preferences", async (done) => { + it("displays initial view with form and listing preferences", async () => { axios.get.mockResolvedValue({ data: lotteryResultRentalThree }) const { findByText, asFragment } = render( @@ -30,10 +30,9 @@ describe("ListingDetailsLotteryModal", () => { expect(await findByText("Lottery Results")).toBeDefined() expect(asFragment()).toMatchSnapshot() - done() }) - it("displays error when user submits form with empty lottery number", async (done) => { + it("displays error when user submits form with empty lottery number", async () => { const user = userEvent.setup() const { findByText, getByRole } = render( { /> ) - await act(async () => { - await user.click(getByRole("button")) - }) + await user.click(getByRole("button")) expect(await findByText("Please enter a valid lottery number.")).toBeDefined() - done() }) - it("displays error when user submits form with non numeric lottery number", async (done) => { + it("displays error when user submits form with non numeric lottery number", async () => { const user = userEvent.setup() const { findByText, getByRole } = render( { /> ) - await act(async () => { - const input = getByRole("textbox") - await userEvent.type(input, "123abc") - await user.click(getByRole("button")) - }) + const input = getByRole("textbox") + await userEvent.type(input, "123abc") + await user.click(getByRole("button")) expect(await findByText("Please enter a valid lottery number.")).toBeDefined() - done() }) - it("displays three results when lottery number found", async (done) => { + it("displays three results when lottery number found", async () => { axios.get.mockResolvedValue({ data: lotteryResultRentalThree }) const user = userEvent.setup() @@ -80,20 +73,17 @@ describe("ListingDetailsLotteryModal", () => { /> ) - await act(async () => { - const input = getByRole("textbox") - await userEvent.type(input, "123") - await user.click(getByRole("button")) - }) + const input = getByRole("textbox") + await userEvent.type(input, "123") + await user.click(getByRole("button")) expect(await findByText("Your preference ranking")).toBeDefined() expect(await findByText("Displaced Tenant Housing Preference (DTHP)")).toBeDefined() expect(await findByText("Certificate of Preference (COP)")).toBeDefined() expect(await findByText("Live or Work in San Francisco Preference")).toBeDefined() - done() }) - it("displays error when invalid lottery number", async (done) => { + it("displays error when invalid lottery number", async () => { axios.get.mockResolvedValue({ data: lotteryResultRentalInvalidLotteryNumber }) const user = userEvent.setup() @@ -104,17 +94,14 @@ describe("ListingDetailsLotteryModal", () => { /> ) - await act(async () => { - const input = getByRole("textbox") - await userEvent.type(input, "123") - await user.click(getByRole("button")) - }) + const input = getByRole("textbox") + await userEvent.type(input, "123") + await user.click(getByRole("button")) expect(await findByText("The number you entered was not found.")).toBeDefined() - done() }) - it("displays error when api error", async (done) => { + it("displays error when api error", async () => { axios.get.mockResolvedValue({ data: null }) const user = userEvent.setup() @@ -125,15 +112,12 @@ describe("ListingDetailsLotteryModal", () => { /> ) - await act(async () => { - const input = getByRole("textbox") - await userEvent.type(input, "123") - await user.click(getByRole("button")) - }) + const input = getByRole("textbox") + await userEvent.type(input, "123") + await user.click(getByRole("button")) expect( await findByText("We seem to be having a connection issue. Please try your search again.") ).toBeDefined() - done() }) }) diff --git a/app/javascript/__tests__/modules/listingDetailsLottery/__snapshots__/ListingDetailsLotteryPreferences.test.tsx.snap b/app/javascript/__tests__/modules/listingDetailsLottery/__snapshots__/ListingDetailsLotteryPreferences.test.tsx.snap index edfbf92372..b5cbc9d5fa 100644 --- a/app/javascript/__tests__/modules/listingDetailsLottery/__snapshots__/ListingDetailsLotteryPreferences.test.tsx.snap +++ b/app/javascript/__tests__/modules/listingDetailsLottery/__snapshots__/ListingDetailsLotteryPreferences.test.tsx.snap @@ -13,19 +13,19 @@ exports[`ListingDetailsLotteryPreferences displays 2 preferences - NRHP, L/W 1`] > Housing Preferences -

    -

    - Ranking in these lists is considered in the order shown here. - - Watch a video on how lottery ranking works. - -

    -

    + > +

    + Ranking in these lists is considered in the order shown here. + + Watch a video on how lottery ranking works. + +

    +
    Housing Preferences -

    -

    - Ranking in these lists is considered in the order shown here. - - Watch a video on how lottery ranking works. - -

    -

    + > +

    + Ranking in these lists is considered in the order shown here. + + Watch a video on how lottery ranking works. + +

    +
    Your preference ranking -

    -

    - Ranking in these lists is considered in the order shown here. - - Watch a video on how lottery ranking works. - -

    -

    + > +

    + Ranking in these lists is considered in the order shown here. + + Watch a video on how lottery ranking works. + +

    +
    Your preference ranking -

    -

    - Ranking in these lists is considered in the order shown here. - - Watch a video on how lottery ranking works. - -

    -

    + > +

    + Ranking in these lists is considered in the order shown here. + + Watch a video on how lottery ranking works. + +

    +
    Your lottery ranking -

    -

    - Ranking in these lists is considered in the order shown here. - - Watch a video on how lottery ranking works. - -

    -

    + > +

    + Ranking in these lists is considered in the order shown here. + + Watch a video on how lottery ranking works. + +

    +
    Housing Preferences -

    -

    - Ranking in these lists is considered in the order shown here. - - Watch a video on how lottery ranking works. - -

    -

    + > +

    + Ranking in these lists is considered in the order shown here. + + Watch a video on how lottery ranking works. + +

    +
    { - it("renders the priority units for Shirley Chisholm listing 2", async (done) => { + it("renders the priority units for Shirley Chisholm listing 2", async () => { const { asFragment, findByText } = render() expect(await findByText(t("listings.customListingType.educator.priorityUnits"))).toBeDefined() expect(asFragment()).toMatchSnapshot() - done() }) - it("renders the priority units for Shirley Chisholm listing 2 with other priority units", async (done) => { + it("renders the priority units for Shirley Chisholm listing 2 with other priority units", async () => { const testListing = { Custom_Listing_Type: "Educator 2: SFUSD employees & public", prioritiesDescriptor: [ @@ -28,6 +27,5 @@ describe("TableSubHeader", () => { expect(await findByText(t("listings.customListingType.educator.priorityUnits"))).toBeDefined() expect(await findByText(t("listings.prioritiesDescriptor.mobilityHearingVision"))).toBeDefined() expect(asFragment()).toMatchSnapshot() - done() }) }) diff --git a/app/javascript/__tests__/modules/listings/__snapshots__/TableSubHeader.test.tsx.snap b/app/javascript/__tests__/modules/listings/__snapshots__/TableSubHeader.test.tsx.snap index 59328a09f8..1ffe3feb2f 100644 --- a/app/javascript/__tests__/modules/listings/__snapshots__/TableSubHeader.test.tsx.snap +++ b/app/javascript/__tests__/modules/listings/__snapshots__/TableSubHeader.test.tsx.snap @@ -2,7 +2,9 @@ exports[`TableSubHeader renders the priority units for Shirley Chisholm listing 2 1`] = ` -
    +
    Includes Priority Units for:
      -
      +
      Includes Priority Units for:
        within(container).getByRole("link", { name: text }) describe("", () => { - it("shows the correct header text", async (done) => { + beforeEach(() => { + // The below line prevents @axe-core from throwing an error + // when the html tag does not have a lang attribute + document.documentElement.lang = "en" + }) + + it("shows the correct header text", async () => { const { getByText } = await renderAndLoadAsync() expect(getByText("Apply for affordable housing")).not.toBeNull() - done() }) - it("shows the correct footer logo path", async (done) => { + it("shows the correct footer logo path", async () => { const { getByTestId } = await renderAndLoadAsync( ) const sfLogo = getByTestId("footer-logo-test-id") expect(sfLogo).not.toBeNull() expect(sfLogo).toHaveAttribute("src", "/public/logo.png") - done() }) describe("Main page content", () => { - it("renders a rent button", async (done) => { + it("renders a rent button", async () => { const { getByTestId } = await renderAndLoadAsync() const mainContentContainer = getByTestId("main-content-test-id") expect(getLinkByText(mainContentContainer, "Rent")).toHaveAttribute( "href", "/listings/for-rent" ) - done() }) - it("renders a buy button", async (done) => { + it("renders a buy button", async () => { const { getByTestId } = await renderAndLoadAsync() const mainContentContainer = getByTestId("main-content-test-id") expect(getLinkByText(mainContentContainer, "Buy")).toHaveAttribute( "href", "/listings/for-sale" ) - done() }) - it("renders the email sign up link", async (done) => { + it("renders the email sign up link", async () => { const { getByTestId } = await renderAndLoadAsync() const mainContentContainer = getByTestId("main-content-test-id") const signUpLink = getLinkByText(mainContentContainer, "Sign Up today") @@ -55,7 +57,6 @@ describe("", () => { "href", "https://confirmsubscription.com/h/y/C3BAFCD742D47910" ) - done() }) }) }) diff --git a/app/javascript/__tests__/pages/SignIn.test.tsx b/app/javascript/__tests__/pages/SignIn.test.tsx index f6e584a325..5bc2ccde50 100644 --- a/app/javascript/__tests__/pages/SignIn.test.tsx +++ b/app/javascript/__tests__/pages/SignIn.test.tsx @@ -11,12 +11,11 @@ jest.mock("react-helmet-async", () => { }) describe("", () => { - it("shows the correct form text", async (done) => { + it("shows the correct form text", async () => { const { getAllByText, getByText } = await renderAndLoadAsync() expect(getAllByText("Sign In")).toHaveLength(4) expect(getByText("Email")).not.toBeNull() expect(getByText("Password")).not.toBeNull() expect(getByText("Don't have an account?")).not.toBeNull() - done() }) }) diff --git a/app/javascript/__tests__/pages/getAssistance/additionalResources.test.tsx b/app/javascript/__tests__/pages/getAssistance/additionalResources.test.tsx index e8466cb6b7..2a0ce6ae5b 100644 --- a/app/javascript/__tests__/pages/getAssistance/additionalResources.test.tsx +++ b/app/javascript/__tests__/pages/getAssistance/additionalResources.test.tsx @@ -5,20 +5,18 @@ import { within } from "@testing-library/dom" import { t } from "@bloom-housing/ui-components" describe("", () => { - it("shows the correct header text", async (done) => { + it("shows the correct header text", async () => { const { getByTestId } = await renderAndLoadAsync() const header = getByTestId("page-header") expect( within(header).getByText(t("assistance.title.additionalHousingOpportunities")) ).not.toBeNull() - done() }) - it("shows the correct subtitle text", async (done) => { + it("shows the correct subtitle text", async () => { const { getByText } = await renderAndLoadAsync() expect(getByText(t("assistance.subtitle.additionalHousingOpportunities"))).not.toBeNull() - done() }) }) diff --git a/app/javascript/__tests__/pages/getAssistance/documentChecklist.test.tsx b/app/javascript/__tests__/pages/getAssistance/documentChecklist.test.tsx index e11cae2d3d..fa21ca5b7c 100644 --- a/app/javascript/__tests__/pages/getAssistance/documentChecklist.test.tsx +++ b/app/javascript/__tests__/pages/getAssistance/documentChecklist.test.tsx @@ -5,18 +5,16 @@ import { within } from "@testing-library/dom" import { t } from "@bloom-housing/ui-components" describe("", () => { - it("shows the correct header text", async (done) => { + it("shows the correct header text", async () => { const { getByTestId } = await renderAndLoadAsync() const header = getByTestId("page-header") expect(within(header).getByText(t("assistance.title.documentChecklist"))).not.toBeNull() - done() }) - it("shows the correct subtitle text", async (done) => { + it("shows the correct subtitle text", async () => { const { getByText } = await renderAndLoadAsync() expect(getByText(t("assistance.subtitle.documentChecklist"))).not.toBeNull() - done() }) }) diff --git a/app/javascript/__tests__/pages/getAssistance/getAssistance.test.tsx b/app/javascript/__tests__/pages/getAssistance/getAssistance.test.tsx index 597695a98c..d444b7a0b0 100644 --- a/app/javascript/__tests__/pages/getAssistance/getAssistance.test.tsx +++ b/app/javascript/__tests__/pages/getAssistance/getAssistance.test.tsx @@ -5,22 +5,20 @@ import { within } from "@testing-library/dom" import { t } from "@bloom-housing/ui-components" describe("", () => { - it("shows the correct header text", async (done) => { + it("shows the correct header text", async () => { const { getByTestId } = await renderAndLoadAsync() const mainContent = getByTestId("page-header") expect(within(mainContent).getByText(t("assistance.title.getAssistance"))).not.toBeNull() - done() }) - it("shows the correct subtitle text", async (done) => { + it("shows the correct subtitle text", async () => { const { getByText } = await renderAndLoadAsync() expect(getByText(t("assistance.subtitle.getAssistance"))).not.toBeNull() - done() }) - it("shows the correct section title text", async (done) => { + it("shows the correct section title text", async () => { const { getByText } = await renderAndLoadAsync() expect(getByText(t("assistance.title.housingCouneslors"))).not.toBeNull() @@ -28,6 +26,5 @@ describe("", () => { expect(getByText(t("assistance.title.sfServices"))).not.toBeNull() expect(getByText(t("assistance.title.documentChecklist"))).not.toBeNull() expect(getByText(t("assistance.title.dahliaVideos"))).not.toBeNull() - done() }) }) diff --git a/app/javascript/__tests__/pages/getAssistance/housingCounselors.test.tsx b/app/javascript/__tests__/pages/getAssistance/housingCounselors.test.tsx index ff283af818..525d93a89a 100644 --- a/app/javascript/__tests__/pages/getAssistance/housingCounselors.test.tsx +++ b/app/javascript/__tests__/pages/getAssistance/housingCounselors.test.tsx @@ -20,18 +20,16 @@ describe("", () => { }) }) - it("shows the correct header text", async (done) => { + it("shows the correct header text", async () => { const { getByTestId } = await renderAndLoadAsync() const header = getByTestId("page-header") expect(within(header).getByText(t("assistance.title.housingCouneslors"))).not.toBeNull() - done() }) - it("shows the correct subtitle text", async (done) => { + it("shows the correct subtitle text", async () => { const { getByText } = await renderAndLoadAsync() expect(getByText(t("assistance.subtitle.housingCouneslors"))).not.toBeNull() - done() }) }) diff --git a/app/javascript/__tests__/pages/listings/ForRent.test.tsx b/app/javascript/__tests__/pages/listings/ForRent.test.tsx index 64f51893a2..0cc7733e6f 100644 --- a/app/javascript/__tests__/pages/listings/ForRent.test.tsx +++ b/app/javascript/__tests__/pages/listings/ForRent.test.tsx @@ -21,17 +21,16 @@ describe("For Rent", () => { jest.resetAllMocks() }) - it("renders ForRent component", async (done) => { + it("renders ForRent component", async () => { axios.get.mockResolvedValue({ data: { listings: [] } }) const { findByText, asFragment } = render() expect(await findByText("Rent affordable housing")).toBeDefined() expect(asFragment()).toMatchSnapshot() - done() }) - it("listings with multiple listings render the first image in the array", async (done) => { + it("listings with multiple listings render the first image in the array", async () => { axios.get.mockResolvedValue({ data: { listings: [sroRentalListing] } }) const { findByAltText } = render() @@ -51,6 +50,5 @@ describe("For Rent", () => { const image = await findByAltText("This is a listing image") expect(image.getAttribute("src")).toBe(sroRentalListing.Listing_Images[0].displayImageURL) - done() }) }) diff --git a/app/javascript/__tests__/pages/listings/ForSale.test.tsx b/app/javascript/__tests__/pages/listings/ForSale.test.tsx index df1f3049ef..288ca7c6c7 100644 --- a/app/javascript/__tests__/pages/listings/ForSale.test.tsx +++ b/app/javascript/__tests__/pages/listings/ForSale.test.tsx @@ -21,23 +21,21 @@ describe("For Sale", () => { jest.resetAllMocks() }) - it("renders ForSale component", async (done) => { + it("renders ForSale component", async () => { axios.get.mockResolvedValue({ data: { listings: [] } }) const { findByText, asFragment } = render() expect(await findByText("Buy affordable housing")).toBeDefined() expect(asFragment()).toMatchSnapshot() - done() }) - it("listings with multiple listings render the first image in the array", async (done) => { + it("listings with multiple listings render the first image in the array", async () => { axios.get.mockResolvedValue({ data: { listings: [openSaleListing] } }) const { findByAltText } = render() const image = await findByAltText("This is a listing image") expect(image.getAttribute("src")).toBe(openSaleListing.Listing_Images[0].displayImageURL) - done() }) }) diff --git a/app/javascript/__tests__/pages/listings/__snapshots__/ForRent.test.tsx.snap b/app/javascript/__tests__/pages/listings/__snapshots__/ForRent.test.tsx.snap index db8420a0b2..e03397a98f 100644 --- a/app/javascript/__tests__/pages/listings/__snapshots__/ForRent.test.tsx.snap +++ b/app/javascript/__tests__/pages/listings/__snapshots__/ForRent.test.tsx.snap @@ -8,7 +8,9 @@ exports[`For Rent renders ForRent component 1`] = `
        - + <title + class="notranslate" + > Rental Listings - + <title + class="notranslate" + > Sale Listings { + return { + nanoid: () => {}, + } +}) + beforeEach(() => { jest.resetAllMocks() }) diff --git a/app/javascript/__tests__/util/languageUtil.test.ts b/app/javascript/__tests__/util/languageUtil.test.ts index 2413d1da34..79605c33e7 100644 --- a/app/javascript/__tests__/util/languageUtil.test.ts +++ b/app/javascript/__tests__/util/languageUtil.test.ts @@ -2,8 +2,6 @@ import { getCurrentLanguage, getPathWithoutLanguagePrefix, getRoutePrefix, - LanguagePrefix, - toLanguagePrefix, getReservedCommunityType, defaultIfNotTranslated, localizedFormat, @@ -12,20 +10,6 @@ import { } from "../../util/languageUtil" describe("languageUtil", () => { - describe("toLanguagePrefix", () => { - it("returns the correct prefix when a blank or invalid string is provided", () => { - expect(toLanguagePrefix("")).toBe(LanguagePrefix.English) - expect(toLanguagePrefix("abc")).toBe(LanguagePrefix.English) - }) - - it("returns the correct prefix", () => { - expect(toLanguagePrefix("en")).toBe(LanguagePrefix.English) - expect(toLanguagePrefix("es")).toBe(LanguagePrefix.Spanish) - expect(toLanguagePrefix("zh")).toBe(LanguagePrefix.Chinese) - expect(toLanguagePrefix("tl")).toBe(LanguagePrefix.Tagalog) - }) - }) - describe("getRoutePrefix", () => { it("gets the prefix from the url when one exists with a leading slash", () => { expect(getRoutePrefix("/en/sign-in")).toBe("en") diff --git a/app/javascript/__tests__/util/listingUtil.test.ts b/app/javascript/__tests__/util/listingUtil.test.ts index efd7bcaefb..c4cf4e78f2 100644 --- a/app/javascript/__tests__/util/listingUtil.test.ts +++ b/app/javascript/__tests__/util/listingUtil.test.ts @@ -37,6 +37,10 @@ import { sroRentalListing } from "../data/RailsRentalListing/listing-rental-sro" import { unitsWithOccupancyAndMaxIncome, units } from "../data/RailsListingUnits/listing-units" import { amiCharts } from "../data/RailsAmiCharts/ami-charts" import { groupedUnitsByOccupancy } from "../data/RailsListingUnits/grouped-units-by-occupancy" +import RailsUnit, { + RailsUnitWithOccupancy, + RailsUnitWithOccupancyAndMinMaxIncome, +} from "../../api/types/rails/listings/RailsUnit" describe("listingUtil", () => { const OLD_ENV = process.env @@ -275,7 +279,7 @@ describe("deriveIncomeFromAmiCharts", () => { describe("addUnitsWithEachOccupancy", () => { it("should return an empty array when units is empty", () => { - const units = [] + const units: Array = [] const result = addUnitsWithEachOccupancy(units) expect(result).toEqual([]) }) @@ -293,7 +297,7 @@ describe("addUnitsWithEachOccupancy", () => { describe("buildAmiArray", () => { it("should return an empty array when units is empty", () => { - const units = [] + const units: Array = [] const result = buildAmiArray(units) expect(result).toEqual([]) }) @@ -382,7 +386,7 @@ describe("matchSharedUnitFields", () => { }) it("should handle an empty input array", () => { - const inputUnits = [] + const inputUnits: Array = [] const expectedOutput = inputUnits const actualOutput = matchSharedUnitFields(inputUnits) expect(actualOutput).toEqual(expectedOutput) @@ -429,7 +433,7 @@ describe("buildOccupanciesArray", () => { }) it("should handle an empty input array", () => { - const inputUnits = [] + const inputUnits: Array = [] const expectedOutput = [] const actualOutput = buildOccupanciesArray(inputUnits) expect(actualOutput).toEqual(expectedOutput) @@ -452,7 +456,7 @@ describe("groupAndSortUnitsByOccupancy", () => { describe("getAmiChartDataFromUnits", () => { test("returns empty array when given empty array of units", () => { - const units = [] + const units: Array = [] const result = getAmiChartDataFromUnits(units) expect(result).toEqual([]) }) @@ -481,9 +485,12 @@ describe("getPriorityTypeText", () => { ${"Hearing/Vision impairments"} | ${"Vision and/or Hearing Impairments"} ${"Mobility/Hearing/Vision impairments"} | ${"Mobility, Hearing and/or Vision Impairments"} ${"Mobility impairments"} | ${"Mobility Impairments"} - `("returns text $text when priority type is $priorityType", ({ priorityType, text }) => { - expect(getPriorityTypeText(priorityType)).toBe(text) - }) + `( + "returns text $text when priority type is $priorityType", + ({ priorityType, text }: { priorityType: string; text: string }) => { + expect(getPriorityTypeText(priorityType)).toBe(text) + } + ) }) describe("getTagContent", () => { diff --git a/app/javascript/api/apiService.ts b/app/javascript/api/apiService.ts index 2a33f8c645..e29c74b598 100644 --- a/app/javascript/api/apiService.ts +++ b/app/javascript/api/apiService.ts @@ -1,4 +1,5 @@ -import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios" +import axios, { AxiosInstance, AxiosResponse } from "axios" +import type { AxiosRequestConfig } from "axios" import { setAuthHeaders, getHeaders } from "../authentication/token" @@ -7,14 +8,13 @@ const createAxiosInstance = (): AxiosInstance => { if (!getHeaders()) { throw new Error("Unauthorized. Sign in first") } - return axios.create({ headers: getHeaders(), transformResponse: (res, headers) => { if (headers["access-token"]) { setAuthHeaders(headers) } - return JSON.parse(res) + return JSON.parse(res as string) }, }) } diff --git a/app/javascript/api/authApiService.ts b/app/javascript/api/authApiService.ts index f574baa91e..dbab8ccd1b 100644 --- a/app/javascript/api/authApiService.ts +++ b/app/javascript/api/authApiService.ts @@ -1,13 +1,14 @@ -import { setAuthHeaders } from "../authentication/token" +import { AxiosResponse } from "axios" import { User, UserData } from "../authentication/user" import { authenticatedGet, post, put } from "./apiService" +import { AuthHeaders, setAuthHeaders } from "../authentication/token" export const signIn = async (email: string, password: string): Promise => post("/api/v1/auth/sign_in", { email, password, - }).then(({ data, headers }) => { - setAuthHeaders(headers) + }).then(({ data, headers }: AxiosResponse) => { + setAuthHeaders(headers as AuthHeaders) return data.data }) diff --git a/app/javascript/api/types/rails/listings/RailsUnit.d.ts b/app/javascript/api/types/rails/listings/RailsUnit.d.ts index 5eccaf8952..086a0fb874 100644 --- a/app/javascript/api/types/rails/listings/RailsUnit.d.ts +++ b/app/javascript/api/types/rails/listings/RailsUnit.d.ts @@ -21,10 +21,10 @@ type RailsUnit = { Unit_Square_Footage?: number attributes: ListingAttributes isReservedCommunity: boolean - Price_Without_Parking?: any - Price_With_Parking?: any - HOA_Dues_Without_Parking?: any - HOA_Dues_With_Parking?: any + Price_Without_Parking?: number | string // Eventually these get transformed from numbers into price range strings, to be used in the pricing table + Price_With_Parking?: number | string + HOA_Dues_Without_Parking?: number | string + HOA_Dues_With_Parking?: number | string Priority_Type?: string Rent_percent_of_income?: number } diff --git a/app/javascript/authentication/token.ts b/app/javascript/authentication/token.ts index 4581021cf2..0f18996397 100644 --- a/app/javascript/authentication/token.ts +++ b/app/javascript/authentication/token.ts @@ -1,3 +1,4 @@ +import { AxiosHeaders } from "axios" const ACCESS_TOKEN_LOCAL_STORAGE_KEY = "auth_headers" const getStorage = () => { @@ -11,12 +12,12 @@ const getStorage = () => { } } -const getAuthHeaders = (): AuthHeaders | undefined => { - const headers = getStorage()[ACCESS_TOKEN_LOCAL_STORAGE_KEY] +const getAuthHeaders = (): AuthHeaders | AxiosHeaders | undefined => { + const headers: string = getStorage()[ACCESS_TOKEN_LOCAL_STORAGE_KEY] return headers && JSON.parse(headers) } -export interface AuthHeaders { +export interface AuthHeaders extends AxiosHeaders { expiry: string "access-token": string client: string @@ -24,7 +25,7 @@ export interface AuthHeaders { "token-type": string } -export const setAuthHeaders = (headers: AuthHeaders) => { +export const setAuthHeaders = (headers: AuthHeaders | AxiosHeaders) => { // Set only relevant auth headers const headersToSet = { expiry: headers.expiry, @@ -36,8 +37,9 @@ export const setAuthHeaders = (headers: AuthHeaders) => { getStorage().setItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY, JSON.stringify(headersToSet)) } -export const getHeaders = (): AuthHeaders | undefined => getAuthHeaders() +export const getHeaders = (): AuthHeaders | AxiosHeaders | undefined => getAuthHeaders() export const clearHeaders = () => getStorage().removeItem(ACCESS_TOKEN_LOCAL_STORAGE_KEY) -const getTokenTtl = (): number => Number.parseInt(getAuthHeaders()?.expiry) * 1000 - Date.now() +const getTokenTtl = (): number => + Number.parseInt(getAuthHeaders()?.expiry as string) * 1000 - Date.now() export const isTokenValid = (): boolean => getAuthHeaders() && getTokenTtl() > 0 diff --git a/app/javascript/contexts/listingDetails/listingDetailsProvider.tsx b/app/javascript/contexts/listingDetails/listingDetailsProvider.tsx index 595e2d4686..725013a944 100644 --- a/app/javascript/contexts/listingDetails/listingDetailsProvider.tsx +++ b/app/javascript/contexts/listingDetails/listingDetailsProvider.tsx @@ -47,7 +47,7 @@ const ListingDetailsProvider = (props: ListingDetailsProviderProps) => { .then((units: RailsUnit[]) => { dispatch(finishFetchingUnits(units)) }) - .catch((error) => { + .catch((error: Error) => { console.error(error) dispatch(setFetchingUnitsError(error)) }) @@ -58,7 +58,7 @@ const ListingDetailsProvider = (props: ListingDetailsProviderProps) => { .then((amiCharts: RailsAmiChart[]) => { dispatch(finishFetchingAmiCharts({ amiCharts, chartsToFetch })) }) - .catch((error) => { + .catch((error: Error) => { console.error(error) dispatch(setFetchingAmiChartsError(error)) }) diff --git a/app/javascript/hooks/useScript.tsx b/app/javascript/hooks/useScript.tsx index ee0f879e1f..b93d33fbc4 100644 --- a/app/javascript/hooks/useScript.tsx +++ b/app/javascript/hooks/useScript.tsx @@ -1,3 +1,6 @@ +/* eslint-disable unicorn/prefer-dom-node-dataset */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ import { useState, useEffect } from "react" const useScript = (src) => { @@ -14,21 +17,18 @@ const useScript = (src) => { // Fetch existing script element by src // It may have been added by another intance of this hook // TODO(DAH-1581): Remove any type on line 18 - // eslint-disable-next-line @typescript-eslint/no-explicit-any let script: any = document.querySelector(`script[src="${src}"]`) if (!script) { // Create script script = document.createElement("script") script.src = src script.async = true - // eslint-disable-next-line unicorn/prefer-dom-node-dataset script.setAttribute("data-status", "loading") // Add script to document body document.body.append(script) // Store status in attribute on script // This can be read by other instances of this hook const setAttributeFromEvent = (event) => { - // eslint-disable-next-line unicorn/prefer-dom-node-dataset script.setAttribute("data-status", event.type === "load" ? "ready" : "error") } script.addEventListener("load", setAttributeFromEvent) diff --git a/app/javascript/layouts/MetaTags.tsx b/app/javascript/layouts/MetaTags.tsx index 8a35a3e72a..abd6c72bd1 100644 --- a/app/javascript/layouts/MetaTags.tsx +++ b/app/javascript/layouts/MetaTags.tsx @@ -4,6 +4,7 @@ import { t } from "@bloom-housing/ui-components" import { Helmet } from "react-helmet-async" import { ConfigContext } from "../lib/ConfigContext" +import { getCurrentLanguage } from "../util/languageUtil" export interface MetaTagsProps { title?: string @@ -13,11 +14,14 @@ export interface MetaTagsProps { const MetaTags = (props: MetaTagsProps) => { const { getAssetPath } = useContext(ConfigContext) + const lang = getCurrentLanguage(window.location.pathname) // Description is separated into two check as Helmet can't handle nested elements return ( <> - - {props.title || t("t.dahliaSanFranciscoHousingPortal")} + + + {props.title || t("t.dahliaSanFranciscoHousingPortal")} + diff --git a/app/javascript/modules/listingDetails/ListingDetailsFeatures.tsx b/app/javascript/modules/listingDetails/ListingDetailsFeatures.tsx index f44ea5c6e3..66c16a3a18 100644 --- a/app/javascript/modules/listingDetails/ListingDetailsFeatures.tsx +++ b/app/javascript/modules/listingDetails/ListingDetailsFeatures.tsx @@ -21,7 +21,7 @@ const getDepositString = (min?: string, max?: string) => { // TODO: add prop for items that should have machine translated content interface FeatureItemProps { - content: any + content: string title: string toTranslate?: boolean } @@ -159,7 +159,7 @@ export const ListingDetailsFeatures = ({ listing, imageSrc }: ListingDetailsFeat )} applicationFee={listing.Fee ? `$${listing.Fee.toFixed(2)?.toLocaleString()}` : null} footerContent={[ -

        {renderMarkup(listing.Costs_Not_Included)}

        , + {renderMarkup(listing.Costs_Not_Included)}, ]} strings={{ sectionHeader: t("listings.features.additionalFees"), diff --git a/app/javascript/modules/listingDetails/ListingDetailsHMITable.tsx b/app/javascript/modules/listingDetails/ListingDetailsHMITable.tsx index dc2113f97b..3502585122 100644 --- a/app/javascript/modules/listingDetails/ListingDetailsHMITable.tsx +++ b/app/javascript/modules/listingDetails/ListingDetailsHMITable.tsx @@ -195,13 +195,18 @@ export const ListingDetailsHMITable = ({ listing }: ListingDetailsEligibilityPro const [tableCollapsed, setTableCollapsed] = useState(true) - const { minOccupancy, maxOccupancy, explicitMaxOccupancy } = useMemo(() => { - if (!fetchedAmiCharts || !fetchedUnits) { - return { minOccupancy: undefined, maxOccupnacy: undefined } - } + const { + minOccupancy, + maxOccupancy, + explicitMaxOccupancy, + }: { minOccupancy?: number; maxOccupancy?: number; explicitMaxOccupancy?: boolean } = + useMemo(() => { + if (!fetchedAmiCharts || !fetchedUnits) { + return { minOccupancy: undefined, maxOccupnacy: undefined } + } - return getMinMaxOccupancy(units, amiCharts) - }, [units, amiCharts, fetchedAmiCharts, fetchedUnits]) + return getMinMaxOccupancy(units, amiCharts) + }, [units, amiCharts, fetchedAmiCharts, fetchedUnits]) const HMITableData = useMemo(() => { if (!fetchedAmiCharts || !fetchedUnits) { diff --git a/app/javascript/modules/listingDetails/ListingDetailsHabitat.tsx b/app/javascript/modules/listingDetails/ListingDetailsHabitat.tsx index 2dbfe725a3..eec036c74c 100644 --- a/app/javascript/modules/listingDetails/ListingDetailsHabitat.tsx +++ b/app/javascript/modules/listingDetails/ListingDetailsHabitat.tsx @@ -25,13 +25,13 @@ export const ListingDetailsHabitat = ({ listing }: ListingDetailsHabitatProps) = {t("listings.habitat.applicationProcess.title")} -

        + {renderMarkup( t("listings.habitat.applicationProcess.p1", { habitatLink: "https://habitatgsf.org/amber-drive-info/", }) )} -

        +
        1. {renderMarkup( @@ -55,7 +55,7 @@ export const ListingDetailsHabitat = ({ listing }: ListingDetailsHabitatProps) =

          {t("listings.habitat.incomeRange.p1")}

          {t("listings.habitat.incomeRange.p2")}

          -

          +

          {renderMarkup( t("listings.incomeExceptions.intro", { url: getSfGovUrl( @@ -64,7 +64,7 @@ export const ListingDetailsHabitat = ({ listing }: ListingDetailsHabitatProps) = ), }) )} -

          +
          • {t("listings.incomeExceptions.students")}
          • {t("listings.incomeExceptions.nontaxable")}
          • diff --git a/app/javascript/modules/listingDetails/ListingDetailsPricingTable.tsx b/app/javascript/modules/listingDetails/ListingDetailsPricingTable.tsx index 04d9ff3062..dd685d36e4 100644 --- a/app/javascript/modules/listingDetails/ListingDetailsPricingTable.tsx +++ b/app/javascript/modules/listingDetails/ListingDetailsPricingTable.tsx @@ -30,11 +30,11 @@ const buildSalePriceCellRow = (unit: RailsUnitWithOccupancyAndMinMaxIncome) => { if (unit.Price_With_Parking && unit.Price_Without_Parking) { return [ { - cellText: unit.Price_With_Parking, + cellText: String(unit.Price_With_Parking), cellSubText: t("listings.stats.withParking"), }, { - cellText: unit.Price_Without_Parking, + cellText: String(unit.Price_Without_Parking), cellSubText: t("listings.stats.withoutParking"), }, ] @@ -43,7 +43,7 @@ const buildSalePriceCellRow = (unit: RailsUnitWithOccupancyAndMinMaxIncome) => { if (unit.Price_With_Parking && !unit.Price_Without_Parking) { return [ { - cellText: unit.Price_With_Parking, + cellText: String(unit.Price_With_Parking), cellSubText: t("listings.stats.withParking"), }, ] @@ -52,7 +52,7 @@ const buildSalePriceCellRow = (unit: RailsUnitWithOccupancyAndMinMaxIncome) => { if (!unit.Price_With_Parking && unit.Price_Without_Parking) { return [ { - cellText: unit.Price_Without_Parking, + cellText: String(unit.Price_Without_Parking), cellSubText: t("listings.stats.withoutParking"), }, ] @@ -63,11 +63,11 @@ const buildSaleHoaDuesCellRow = (unit: RailsUnitWithOccupancyAndMinMaxIncome) => if (unit?.HOA_Dues_With_Parking && unit?.HOA_Dues_Without_Parking) { return [ { - cellText: unit.HOA_Dues_With_Parking, + cellText: String(unit.HOA_Dues_With_Parking), cellSubText: t("listings.stats.withParking"), }, { - cellText: unit.HOA_Dues_Without_Parking, + cellText: String(unit.HOA_Dues_Without_Parking), cellSubText: t("listings.stats.withoutParking"), }, ] @@ -76,7 +76,7 @@ const buildSaleHoaDuesCellRow = (unit: RailsUnitWithOccupancyAndMinMaxIncome) => if (unit?.HOA_Dues_With_Parking && !unit?.HOA_Dues_Without_Parking) { return [ { - cellText: unit.HOA_Dues_With_Parking, + cellText: String(unit.HOA_Dues_With_Parking), cellSubText: t("listings.stats.withParking"), }, ] @@ -85,7 +85,7 @@ const buildSaleHoaDuesCellRow = (unit: RailsUnitWithOccupancyAndMinMaxIncome) => if (!unit?.HOA_Dues_With_Parking && unit?.HOA_Dues_Without_Parking) { return [ { - cellText: unit.HOA_Dues_Without_Parking, + cellText: String(unit.HOA_Dues_Without_Parking), cellSubText: t("listings.stats.withoutParking"), }, ] diff --git a/app/javascript/modules/listingDetails/ListingDetailsUnitAccordions.tsx b/app/javascript/modules/listingDetails/ListingDetailsUnitAccordions.tsx index d3061e5983..4b4dd0561a 100644 --- a/app/javascript/modules/listingDetails/ListingDetailsUnitAccordions.tsx +++ b/app/javascript/modules/listingDetails/ListingDetailsUnitAccordions.tsx @@ -45,7 +45,14 @@ const getTableData = (units: RailsUnit[]) => { }) } -const sortUnits = (units) => { +type UnitType = { + units: RailsUnit[] + availability: number + minSqFt: number + maxSqFt: number +} + +const sortUnits = (units: RailsUnit[]): Record => { return units?.reduce((acc, unit) => { if (!acc[unit.Unit_Type]) { acc = { diff --git a/app/javascript/modules/listingDetailsLottery/ListingDetailsLotteryPreferences.tsx b/app/javascript/modules/listingDetailsLottery/ListingDetailsLotteryPreferences.tsx index a1f704f7bf..f295ee02e7 100644 --- a/app/javascript/modules/listingDetailsLottery/ListingDetailsLotteryPreferences.tsx +++ b/app/javascript/modules/listingDetailsLottery/ListingDetailsLotteryPreferences.tsx @@ -16,13 +16,13 @@ export const ListingDetailsLotteryPreferences = ({ {t("lottery.housingPreferences")} -

            +

            {renderMarkup( `${t("lottery.rankingOrderNote", { lotteryRankingVideoUrl: "https://www.youtube.com/watch?v=4ZB35gagUl8", })}` )} -

            +
            {lotteryBucketsDetails.lotteryBuckets .filter( diff --git a/app/javascript/modules/listingDetailsLottery/ListingDetailsLotteryRanking.tsx b/app/javascript/modules/listingDetailsLottery/ListingDetailsLotteryRanking.tsx index cce1d59bb9..b1fd32bc67 100644 --- a/app/javascript/modules/listingDetailsLottery/ListingDetailsLotteryRanking.tsx +++ b/app/javascript/modules/listingDetailsLottery/ListingDetailsLotteryRanking.tsx @@ -47,13 +47,13 @@ export const ListingDetailsLotteryRanking = ({ ? t("lottery.rankingTitle") : t("lottery.rankingTitle.noPreference")} -

            +

            {renderMarkup( `${t("lottery.rankingOrderNote", { lotteryRankingVideoUrl: "https://www.youtube.com/watch?v=4ZB35gagUl8", })}` )} -

            +
            {applicantSelectedForPreference && !applicantHasCertOfPreference && ( diff --git a/app/javascript/modules/listingDetailsLottery/ListingDetailsLotteryResults.tsx b/app/javascript/modules/listingDetailsLottery/ListingDetailsLotteryResults.tsx index 8455b01556..f012703209 100644 --- a/app/javascript/modules/listingDetailsLottery/ListingDetailsLotteryResults.tsx +++ b/app/javascript/modules/listingDetailsLottery/ListingDetailsLotteryResults.tsx @@ -72,6 +72,7 @@ export const ListingDetailsLotteryResults = ({ listing }: ListingDetailsLotteryR setIsModalOpen(false)} open={isModalOpen} + role="alertdialog" title="" modalClassNames="md:max-w-0 w-screen" innerClassNames="p-0" diff --git a/app/javascript/modules/listingDetailsLottery/ListingDetailsLotterySearchForm.tsx b/app/javascript/modules/listingDetailsLottery/ListingDetailsLotterySearchForm.tsx index a40b7987ec..8995cfe59b 100644 --- a/app/javascript/modules/listingDetailsLottery/ListingDetailsLotterySearchForm.tsx +++ b/app/javascript/modules/listingDetailsLottery/ListingDetailsLotterySearchForm.tsx @@ -94,7 +94,7 @@ export const ListingDetailsLotterySearchForm = ({ } const errorMessage = - (["required", "pattern"].includes(errors[lotteryNumberField]?.type) && + (["required", "pattern"].includes(String(errors[lotteryNumberField]?.type)) && t("lottery.lotteryNumberNotValid")) || (lotteryFormStatus === LOTTERY_SEARCH_FORM_STATUS.INVALID_LOTTERY_NUMBER && t("lottery.lotteryNumberNotFoundP1")) || @@ -110,6 +110,7 @@ export const ListingDetailsLotterySearchForm = ({
            Record[] + export interface ListingsGroups { open: RailsListing[] upcoming: RailsListing[] @@ -177,7 +180,12 @@ export const getPriorityTypes = (listing: RailsRentalListing): string[] | null = } // Get a set of Listing Cards for an array of listings, which includes both the image and summary table -export const getListingCards = (listings, directoryType, stackedDataFxn, hasFiltersSet?: boolean) => +export const getListingCards = ( + listings, + directoryType, + stackedDataFxn: StackedDataFxnType, + hasFiltersSet?: boolean +) => listings.map((listing: Listing, index) => { const hasCustomContent = listing.Reserved_community_type === habitatForHumanity return ( @@ -192,6 +200,7 @@ export const getListingCards = (listings, directoryType, stackedDataFxn, hasFilt tableHeader: { content: getTableHeader(listing) }, tableSubheader: { content: , + isElement: true, }, contentHeader: { content: listing.Name, href: `/listings/${listing.listingID}` }, contentSubheader: { content: }, @@ -226,14 +235,18 @@ export const getListingCards = (listings, directoryType, stackedDataFxn, hasFilt ) }) -export const openListingsView = (listings, directoryType, stackedDataFxn, filtersSet?) => - listings.length > 0 && getListingCards(listings, directoryType, stackedDataFxn, filtersSet) +export const openListingsView = ( + listings: RailsListing[], + directoryType: DirectoryType, + stackedDataFxn: StackedDataFxnType, + filtersSet?: boolean +) => listings.length > 0 && getListingCards(listings, directoryType, stackedDataFxn, filtersSet) // Get an expandable group of listings export const getListingGroup = ( listings, directoryType, - stackedDataFxn, + stackedDataFxn: StackedDataFxnType, header, hide, show, @@ -257,7 +270,11 @@ export const getListingGroup = ( ) } -export const upcomingLotteriesView = (listings, directoryType, stackedDataFxn) => { +export const upcomingLotteriesView = ( + listings, + directoryType, + stackedDataFxn: StackedDataFxnType +) => { return getListingGroup( listings, directoryType, @@ -270,7 +287,7 @@ export const upcomingLotteriesView = (listings, directoryType, stackedDataFxn) = ) } -export const lotteryResultsView = (listings, directoryType, stackedDataFxn) => { +export const lotteryResultsView = (listings, directoryType, stackedDataFxn: StackedDataFxnType) => { return getListingGroup( listings, directoryType, @@ -284,7 +301,12 @@ export const lotteryResultsView = (listings, directoryType, stackedDataFxn) => { ) } -export const additionalView = (listings, directoryType, stackedDataFxn, filtersSet?: boolean) => { +export const additionalView = ( + listings, + directoryType, + stackedDataFxn: StackedDataFxnType, + filtersSet?: boolean +) => { return getListingGroup( listings, directoryType, diff --git a/app/javascript/modules/listings/GenericDirectory.tsx b/app/javascript/modules/listings/GenericDirectory.tsx index 8dbfcebcd6..ebe9de896a 100644 --- a/app/javascript/modules/listings/GenericDirectory.tsx +++ b/app/javascript/modules/listings/GenericDirectory.tsx @@ -30,7 +30,7 @@ interface RentalDirectoryProps { } export const GenericDirectory = (props: RentalDirectoryProps) => { - const [rawListings, setRawListings] = useState([]) + const [rawListings, setRawListings] = useState>([]) const [listings, setListings] = useState({ open: [], upcoming: [], diff --git a/app/javascript/modules/listings/HabitatForHumanity.tsx b/app/javascript/modules/listings/HabitatForHumanity.tsx index 5db42298af..9d39119f46 100644 --- a/app/javascript/modules/listings/HabitatForHumanity.tsx +++ b/app/javascript/modules/listings/HabitatForHumanity.tsx @@ -39,7 +39,7 @@ export const getHabitatContent = (listing, stackedDataFxn) => { {t("listings.availableUnits")} {getHeader(t("t.units"))} - {stackedData.map((row, index) => + {stackedData.map((row, index: number) => getHabitatContentRow( `${row.unitType.cellText}:`, `${row.availability.cellText} ${t("t.available")}`, diff --git a/app/javascript/modules/listings/TableSubHeader.tsx b/app/javascript/modules/listings/TableSubHeader.tsx index 1678b61d6f..dbb72ff3ec 100644 --- a/app/javascript/modules/listings/TableSubHeader.tsx +++ b/app/javascript/modules/listings/TableSubHeader.tsx @@ -12,7 +12,7 @@ const TableSubHeader = ({ listing }: TableSubHeaderProps) => { const priorityTypes = getPriorityTypes(listing) return ( (priorityTypes || isEducatorTwo(listing)) && ( -
            +
            {t("listings.includesPriorityUnits")}
              {isEducatorTwo(listing) && ( diff --git a/app/javascript/pages/getAssistance/privacy.tsx b/app/javascript/pages/getAssistance/privacy.tsx index 6fe89c2bee..67ff55fa18 100644 --- a/app/javascript/pages/getAssistance/privacy.tsx +++ b/app/javascript/pages/getAssistance/privacy.tsx @@ -63,7 +63,7 @@ const Privacy = () => {

            {t("privacyPolicy.analyticsTitle")}

            -

            +

            {renderMarkup( `${t("privacyPolicy.analyticsP1", { termsLink: @@ -75,7 +75,7 @@ const Privacy = () => { linkEnd: "", })}` )} -

            +

            @@ -112,7 +112,7 @@ const Privacy = () => {

            {t("privacyPolicy.questionsTitle")}

            -

            +

            {renderMarkup( `${t("privacyPolicy.questionsP1", { telLink: '' + t("415-701-5500") + "", @@ -122,7 +122,7 @@ const Privacy = () => { "", })}` )} -

            +
            diff --git a/app/javascript/util/languageUtil.tsx b/app/javascript/util/languageUtil.tsx index c07fdaa92b..9b71067b97 100644 --- a/app/javascript/util/languageUtil.tsx +++ b/app/javascript/util/languageUtil.tsx @@ -1,6 +1,6 @@ import { t, addTranslation } from "@bloom-housing/ui-components" import Markdown from "markdown-to-jsx" -import dayjs from "dayjs" +import dayjs, { PluginFunc } from "dayjs" import React from "react" import { stripMostTags } from "./filterUtil" @@ -83,7 +83,7 @@ export const loadTranslations = async (prefix: LanguagePrefix): Promise => } // load the plugin for localized formats https://day.js.org/docs/en/plugin/localized-format - const localizedFormat = require("dayjs/plugin/localizedFormat") + const localizedFormat: PluginFunc = require("dayjs/plugin/localizedFormat") dayjs.extend(localizedFormat) // load the locale @@ -92,19 +92,6 @@ export const loadTranslations = async (prefix: LanguagePrefix): Promise => } } -export const toLanguagePrefix = (routePrefix: string | undefined): LanguagePrefix => { - switch (routePrefix) { - case LanguagePrefix.Spanish: - return LanguagePrefix.Spanish - case LanguagePrefix.Chinese: - return LanguagePrefix.Chinese - case LanguagePrefix.Tagalog: - return LanguagePrefix.Tagalog - default: - return LanguagePrefix.English - } -} - /** * Get the language prefix from the url. Or null if no prefix is on the path */ @@ -148,11 +135,11 @@ export const getCurrentLanguage = (path?: string | undefined): LanguagePrefix => export const getSfGovUrl = (enLink: string, node?: number, path?: string) => { if (!SFGOV_LINKS.includes(enLink) || enLink.includes("pdf")) return enLink switch (getCurrentLanguage(path || window.location.pathname)) { - case "es": + case LanguagePrefix.Spanish: return `https://sf.gov/es/node/${node}` - case "tl": + case LanguagePrefix.Tagalog: return `https://sf.gov/fil/node/${node}` - case "zh": + case LanguagePrefix.Chinese: return `https://sf.gov/zh-hant/node/${node}` default: return enLink diff --git a/app/javascript/util/listingUtil.ts b/app/javascript/util/listingUtil.ts index 756b5eb360..396090ecce 100644 --- a/app/javascript/util/listingUtil.ts +++ b/app/javascript/util/listingUtil.ts @@ -140,7 +140,7 @@ export const listingHasSROUnits = (listing: RailsRentalListing | RailsSaleListin * Check if a listing is multi-occupancy SRO * @param {string} name * @param {RailsRentalListing | RailsRentalListing} listing - * @returns {boolean} returns true if the listing is in the harcoded list of SROs that + * @returns {boolean} returns true if the listing is in the hardcoded list of SROs that * permit multiple occupancy, false otherwise */ export const isPluralSRO = (name: string, listing: RailsRentalListing | RailsSaleListing) => { @@ -418,7 +418,7 @@ export const matchSharedUnitFields = ( true ) } - // Update availiability based on availability in matchingUnits + // Update availability based on availability in matchingUnits let numAvailable = 0 matchingUnits.forEach((curUnit: RailsUnitWithOccupancyAndMinMaxIncome) => { numAvailable += curUnit.Availability @@ -576,7 +576,10 @@ export const getLongestAmiChartValueLength = (amiCharts: RailsAmiChart[]): numbe return longestChartLength } -export const getMinMaxOccupancy = (units: RailsUnit[], amiCharts: RailsAmiChart[]): any => { +export const getMinMaxOccupancy = ( + units: RailsUnit[], + amiCharts: RailsAmiChart[] +): { explicitMaxOccupancy: boolean; minOccupancy: number; maxOccupancy: number } => { const unitsCopy = units.map((unit) => { return { ...unit } }) @@ -617,9 +620,9 @@ export const getMinMaxOccupancy = (units: RailsUnit[], amiCharts: RailsAmiChart[ } } -export const getPriorityTypeText = (priortyType: string): string => { +export const getPriorityTypeText = (priorityType: string): string => { let text: string - switch (priortyType) { + switch (priorityType) { case "Vision impairments": text = t("listings.prioritiesDescriptor.vision") break diff --git a/app/javascript/util/routeUtil.ts b/app/javascript/util/routeUtil.ts index 154fd04520..60e2f0139f 100644 --- a/app/javascript/util/routeUtil.ts +++ b/app/javascript/util/routeUtil.ts @@ -4,7 +4,6 @@ import { LangConfig, LANGUAGE_CONFIGS, LanguagePrefix, - toLanguagePrefix, } from "./languageUtil" import { cleanPath } from "./urlUtil" @@ -45,7 +44,12 @@ export const getNewLanguagePath = ( currentPath: string | undefined, newLanguagePrefix: string, queryString: string | undefined -): string => getLocalizedPath(currentPath, toLanguagePrefix(newLanguagePrefix), queryString) +): string => { + if (Object.values(LanguagePrefix).includes(newLanguagePrefix as LanguagePrefix)) { + return getLocalizedPath(currentPath, newLanguagePrefix as LanguagePrefix, queryString) + } + return getLocalizedPath(currentPath, LanguagePrefix.English, queryString) +} export const getHomepagePath = localizedPathGetter("/") export const getRentalDirectoryPath = localizedPathGetter("/listings/for-rent") diff --git a/app/services/db_service.rb b/app/services/db_service.rb new file mode 100644 index 0000000000..76d7f180cf --- /dev/null +++ b/app/services/db_service.rb @@ -0,0 +1,19 @@ +# service for interacting with the postgres database +class DbService + attr_reader :errors + + def initialize + @errors = [] + end + + def cleanup_listing_images + listing_images = ListingImage.where('created_at < ?', 1.year.ago) + Rails.logger.info("Deleting #{listing_images.count} stale listing image records") + + listing_images.destroy_all + true + rescue StandardError => e + Rails.logger.error("Error cleaning up listing images: #{e.class.name}, #{e.message}") + false + end +end diff --git a/app/services/multiple_listing_image_service.rb b/app/services/multiple_listing_image_service.rb index 8800c7d773..253c05297e 100644 --- a/app/services/multiple_listing_image_service.rb +++ b/app/services/multiple_listing_image_service.rb @@ -43,8 +43,7 @@ def process_image(listing_image) create_or_update_listing_image(@listing_id, image_url, li_raw_image_url) end # Else, if the image has been uploaded, check if we need to create the record in Postgres - elsif !listing_image_current?(@listing_id, image_url) || - ENV['FORCE_MULTIPLE_LISTING_IMAGE_UPDATE'].to_s.casecmp('true').zero? + elsif !listing_image_current?(@listing_id, image_url, li_raw_image_url) # if the listing_image record containing the image_url does not exist, create it create_or_update_listing_image(@listing_id, image_url, li_raw_image_url) end @@ -77,8 +76,10 @@ def resized_image?(image_name, resized_listing_images) resized_listing_images.count { |file| file.key.end_with?(image_name) }.positive? end - def listing_image_current?(listing_id, image_url) - ListingImage.where(salesforce_listing_id: listing_id).where(image_url:).exists? + def listing_image_current?(listing_id, image_url, li_raw_image_url) + ListingImage.where(salesforce_listing_id: listing_id, + image_url:, + raw_image_url: li_raw_image_url).exists? end # TODO: pull into a new image_upload service? @@ -122,8 +123,8 @@ def resize_image(image_url, listing_id, tmp_image_path) def create_or_update_listing_image(listing_id, image_url, li_raw_image_url) listing_image = ListingImage.find_or_initialize_by(salesforce_listing_id: listing_id, - image_url:, raw_image_url: li_raw_image_url) - listing_image.update(raw_image_url: li_raw_image_url, image_url:) + raw_image_url: li_raw_image_url) + listing_image.update(image_url:) Rails.logger.info("Listing image for #{listing_id} updated to #{image_url}") end diff --git a/config/application.rb b/config/application.rb index 629982ef54..8c513eb393 100644 --- a/config/application.rb +++ b/config/application.rb @@ -13,11 +13,11 @@ module SfDahliaWeb # setting up config for application class Application < Rails::Application - # uncomment when we are done with config/initializers/new_framework_defaults_7_0.rb - # config.load_defaults 7.0 + config.load_defaults 7.0 config.assets.paths << Rails.root.join('lib', 'assets', 'bower_components') config.assets.paths << Rails.root.join('app', 'assets', 'json', 'translations') + config.assets.paths << Rails.root.join('app', 'assets', 'json', 'translations', 'react') # http://guides.rubyonrails.org/action_mailer_basics.html#previewing-emails config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews" @@ -64,16 +64,10 @@ class Application < Rails::Application r301 '/mohcd-plus-housing', 'https://sfmohcd.org/plus-housing-application' end - # TODO: remove this once we are on Rails 7, only needed as we incrementally upgrade - config.autoloader = :zeitwerk - # Disables the deprecated #to_s override in some Ruby core classes # See https://guides.rubyonrails.org/configuring.html#config-active-support-disable-to-s-conversion for more information. config.active_support.disable_to_s_conversion = true - # Change the format of the cache entry to 7.0 after deploying the 7.0 upgrade - config.active_support.cache_format_version = 6.1 - # Rails 7 can protect from open redirect attacks in `redirect_back_or_to` and `redirect_to`. # This is not compatible with our authentication process so we disable it config.action_controller.raise_on_open_redirects = false diff --git a/config/initializers/jasmine_dependencies.rb b/config/initializers/jasmine_dependencies.rb new file mode 100644 index 0000000000..7ef10a8dd3 --- /dev/null +++ b/config/initializers/jasmine_dependencies.rb @@ -0,0 +1,14 @@ +# Only run if the Jasmine gem is installed, e.g. in test and dev environments +if defined?(Jasmine::Dependencies) + Jasmine::Dependencies.module_eval do + class << self + # override method in the Jasmine gem as a work around for Rails 7 compatibility + # Specifically, work around these lines: + # https://github.com/jasmine/jasmine-gem/blob/d1d2ed7be8443b9ab86cadf0685f50bd2e2402de/lib/jasmine/dependencies.rb#L21 + # https://github.com/jasmine/jasmine-gem/blob/d1d2ed7be8443b9ab86cadf0685f50bd2e2402de/lib/jasmine/asset_expander.rb#L16 + def rails6? + rails? && (Rails.version.to_i == 6 || Rails.version.to_i == 7) + end + end + end +end diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb deleted file mode 100644 index fac64e0a61..0000000000 --- a/config/initializers/new_framework_defaults.rb +++ /dev/null @@ -1,17 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 5.0 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Enable per-form CSRF tokens. Previous versions had false. -Rails.application.config.action_controller.per_form_csrf_tokens = false - -# Enable origin-checking CSRF mitigation. Previous versions had false. -Rails.application.config.action_controller.forgery_protection_origin_check = false - -# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. -# Previous versions had false. -ActiveSupport.to_time_preserves_timezone = false diff --git a/config/initializers/new_framework_defaults_5_2.rb b/config/initializers/new_framework_defaults_5_2.rb deleted file mode 100644 index e4cdd8fe02..0000000000 --- a/config/initializers/new_framework_defaults_5_2.rb +++ /dev/null @@ -1,36 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 5.2 upgrade. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Make Active Record use stable #cache_key alongside new #cache_version method. -# This is needed for recyclable cache keys. -Rails.application.config.active_record.cache_versioning = true - -# Use AES-256-GCM authenticated encryption for encrypted cookies. -# Also, embed cookie expiry in signed or encrypted cookies for increased security. -# -# This option is not backwards compatible with earlier Rails versions. -# It's best enabled when your entire app is migrated and stable on 5.2. -# -# Existing cookies will be converted on read then written with the new scheme. -# Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true - -# Use AES-256-GCM authenticated encryption as default cipher for encrypting messages -# instead of AES-256-CBC, when use_authenticated_message_encryption is set to true. -Rails.application.config.active_support.use_authenticated_message_encryption = true - -# Add default protection from forgery to ActionController::Base instead of in -# ApplicationController. -Rails.application.config.action_controller.default_protect_from_forgery = true - -# Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and -# 'f' after migrating old data. -# Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true - -# Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. -Rails.application.config.active_support.use_sha1_digests = true - -# Make `form_with` generate id attributes for any generated HTML tags. -Rails.application.config.action_view.form_with_generates_ids = true diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb deleted file mode 100644 index 0d33130d71..0000000000 --- a/config/initializers/new_framework_defaults_7_0.rb +++ /dev/null @@ -1,107 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file eases your Rails 7.0 framework defaults upgrade. -# -# Uncomment each configuration one by one to switch to the new default. -# Once your application is ready to run with all new defaults, you can remove -# this file and set the `config.load_defaults` to `7.0`. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. -# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html - -# `button_to` view helper will render `