diff --git a/app/assets/stylesheets/application.tailwind.css.scss b/app/assets/stylesheets/application.tailwind.css.scss index 9947c61a..293caf61 100644 --- a/app/assets/stylesheets/application.tailwind.css.scss +++ b/app/assets/stylesheets/application.tailwind.css.scss @@ -5,5 +5,54 @@ @tailwind utilities; .active { - @apply border-[3px] border-black border-solid border-b-0 text-black -mb-[3px] bg-white; + @apply border-[3px] border-black border-solid border-b-0 -mb-[3px] bg-white; +} + +.after-today { + @apply text-black bg-white; +} + +.daqi-level-1-today, +.daqi-level-2-today, +.daqi-level-3-today { + @apply text-black bg-white; +} + +.inactive { + @apply border-b-0 border-l-2 border-r-2 border-t-2 border-gray-400 border-dashed text-gray-400; +} + +.daqi-level-4-today, +.daqi-alert-after-today-selected-level-4 { + @apply text-black bg-yellow-300; +} + +.daqi-level-5-today, +.daqi-alert-after-today-selected-level-5 { + @apply text-black bg-amber-200; +} + +.daqi-level-6-today, +.daqi-alert-after-today-selected-level-6 { + @apply text-black bg-yellow-500; +} + +.daqi-level-7-today, +.daqi-alert-after-today-selected-level-7 { + @apply text-white bg-orange-500; +} + +.daqi-level-8-today, +.daqi-alert-after-today-selected-level-8 { + @apply text-white bg-red-500; +} + +.daqi-level-9-today, +.daqi-alert-after-today-selected-level-9 { + @apply text-white bg-red-800; +} + +.daqi-level-10-today, +.daqi-alert-after-today-selected-level-10 { + @apply text-white bg-stone-700; } diff --git a/app/components/day_tab_component.html.erb b/app/components/day_tab_component.html.erb new file mode 100644 index 00000000..4512e15f --- /dev/null +++ b/app/components/day_tab_component.html.erb @@ -0,0 +1,33 @@ +<%= tag.div class: "tab py-4 flex-1 #{@day} mx-2 px-1 text-center #{classes}", + data: { + date: @forecast.date.to_s, + controller: "tab", + "tab-active-class": "active", + "tab-inactive-class": "inactive", + "tab-target": "dayPrediction" + } do + %> +<%= link_to update_styled_forecast_path(day: @day, format: :turbo_stream), + data: { + action: "click->tab#switch_tab" + } do %> +
+ <%= @forecast.date == Date.today ? 'Today' : @forecast.date.strftime('%A') %> +
+
+ <%= @forecast.date.strftime("%d %B") %> +
+
+
+ <% if @forecast.air_pollution.value > 3 %> + <%= render partial: "shared/icons/exclamation_triangle", locals: {forecast: @forecast} %> + <% end %> +
+
+ <%= @forecast.air_pollution.label.capitalize %> +
+
+ Index <%= @forecast.air_pollution.value %>/10 +
+<% end %> +<% end %> diff --git a/app/components/day_tab_component.rb b/app/components/day_tab_component.rb new file mode 100644 index 00000000..99d7d929 --- /dev/null +++ b/app/components/day_tab_component.rb @@ -0,0 +1,31 @@ +class DayTabComponent < ViewComponent::Base + def initialize(forecast:, day:) + @forecast = forecast + @day = day + end + + TAG_COLOURS = { + 1 => "bg-lime-400", + 2 => "bg-green-400", + 3 => "bg-lime-600", + 4 => "bg-yellow-300", + 5 => "bg-amber-200", + 6 => "bg-yellow-500", + 7 => "bg-orange-500", + 8 => "bg-red-500", + 9 => "bg-red-800", + 10 => "bg-stone-700" + } + + def daqi_indicator_colour_class + TAG_COLOURS.fetch(@forecast.air_pollution.value) + end + + def classes + if @day == :today + "active daqi-level-#{@forecast.air_pollution.value}-today" + else + "inactive after-today" + end + end +end diff --git a/app/controllers/styled_forecasts_controller.rb b/app/controllers/styled_forecasts_controller.rb index 8c69ff56..6bec411b 100644 --- a/app/controllers/styled_forecasts_controller.rb +++ b/app/controllers/styled_forecasts_controller.rb @@ -9,13 +9,11 @@ def update @maptiler_api_key = ENV.fetch("MAPTILER_API_KEY") forecasts = CercForecastService.latest_forecasts_for(zone).data - day_forecast = forecast_for_day(params.fetch("day"), forecasts) + @day_forecast = forecast_for_day(params.fetch("day"), forecasts) - render turbo_stream: turbo_stream.replace( - "day_predictions", - partial: "predictions", - locals: {forecast: day_forecast} - ) + respond_to do |format| + format.turbo_stream + end end private diff --git a/app/javascript/controllers/tab_controller.js b/app/javascript/controllers/tab_controller.js index 4ebf3ae8..5d5a4fde 100644 --- a/app/javascript/controllers/tab_controller.js +++ b/app/javascript/controllers/tab_controller.js @@ -2,19 +2,58 @@ import { Controller } from "@hotwired/stimulus"; // Connects to data-controller="tab" export default class extends Controller { - static classes = ["active"]; - static targets = ["day"]; + static classes = ["active", "inactive"]; + static targets = ["dayPrediction", "day", "daqiValue"]; connect() {} switch_tab() { this.makeAllTabsInactive(); - this.dayTarget.classList.toggle(this.activeClass); + this.removeDaqiAlertClasses(); + + this.addAlertClassIfNotTodayAndHasAirQualityAlert(); + this.dayPredictionTarget.classList.toggle(this.activeClass); + this.dayPredictionTarget.classList.toggle(this.inactiveClass); } makeAllTabsInactive() { - document - .querySelectorAll(".tabs .tab") - .forEach((el) => el.classList.remove(this.activeClass)); + document.querySelectorAll(".tabs .tab").forEach((el) => { + el.classList.remove(this.activeClass); + el.classList.add(this.inactiveClass); + }); + } + + addAlertClassIfNotTodayAndHasAirQualityAlert() { + const daqiValue = this.getDaqiValue(); + if (this.dayTarget.innerText !== "Today" && daqiValue > 3) { + this.addDaqiAlertClass(); + } + } + + addDaqiAlertClass() { + const daqiValue = this.getDaqiValue(); + this.dayPredictionTarget.classList.add( + `daqi-alert-after-today-selected-level-${daqiValue}` + ); + } + + removeDaqiAlertClasses() { + document.querySelectorAll(".tabs .tab").forEach((el) => { + const daqiAlertClassRegex = new RegExp( + "daqi-alert-after-today-selected-level-(\\d{1,2})" + ); + + const fullClassName = Array.from(el.classList).find((className) => + className.match(daqiAlertClassRegex) + ); + + if (fullClassName) { + el.classList.remove(fullClassName); + } + }); + } + + getDaqiValue() { + return this.daqiValueTarget.innerText.split("/")[0].split(" ")[1]; } } diff --git a/app/views/shared/icons/_exclamation_triangle.html.erb b/app/views/shared/icons/_exclamation_triangle.html.erb new file mode 100644 index 00000000..6fd278bb --- /dev/null +++ b/app/views/shared/icons/_exclamation_triangle.html.erb @@ -0,0 +1,3 @@ + 6 ? "white" : "black"%> class="size-8 -ml-[2.125rem]"> + + diff --git a/app/views/styled_forecasts/_alert_guidance.html.erb b/app/views/styled_forecasts/_alert_guidance.html.erb new file mode 100644 index 00000000..f2c01ea4 --- /dev/null +++ b/app/views/styled_forecasts/_alert_guidance.html.erb @@ -0,0 +1,12 @@ +<%= turbo_frame_tag "alert_guidance" do %> + <% if air_pollution_prediction.air_quality_alert %> +
+

+ <%= t("air_quality_alert.#{air_pollution_prediction.air_quality_alert.daqi_level}.guidance.title") %> +

+
+ <%= t("air_quality_alert.#{air_pollution_prediction.air_quality_alert.daqi_level}.guidance.detail_html") %> +
+
+ <% end %> +<% end %> diff --git a/app/views/styled_forecasts/_day_tab.html.erb b/app/views/styled_forecasts/_day_tab.html.erb deleted file mode 100644 index bafe73b2..00000000 --- a/app/views/styled_forecasts/_day_tab.html.erb +++ /dev/null @@ -1,31 +0,0 @@ -<%= tag.div class: "tab py-4 flex-1 #{day} mx-2 px-1 text-center text-gray-400 daqi-low border-b-0 border-l-2 border-r-2 border-t-2 border-gray-400 border-dashed #{day == :today ? "active" : ''}", - data: { - date: forecast.date.to_s, - controller: "tab", - "tab-active-class": "active", - "tab-target": "day" - } do - %> -<%= - link_to update_styled_forecast_path(day: day), - data: { - turbo_frame: "day_predictions", - action: "click->tab#switch_tab" - } do %> -
- <%= forecast.date == Date.today ? 'Today' : forecast.date.strftime('%A') %> -
-
- <%= forecast.date.strftime("%d %B") %> -
-
- ● -
-
- <%= forecast.air_pollution.label.capitalize %> -
-
- Index <%= forecast.air_pollution.value %>/10 -
-<% end %> -<% end %> diff --git a/app/views/styled_forecasts/_forecast_tabs.html.erb b/app/views/styled_forecasts/_forecast_tabs.html.erb index b15ff8ea..9860d1b4 100644 --- a/app/views/styled_forecasts/_forecast_tabs.html.erb +++ b/app/views/styled_forecasts/_forecast_tabs.html.erb @@ -6,10 +6,11 @@ class="tabs flex flex-row items-stretch mt-4 m-1 text-xs font-bold border-b-[3px] border-black" data-turbo-prefetch="false" > - <%= render "day_tab", forecast: @forecasts.first, day: :today %> - <%= render "day_tab", forecast: @forecasts.second, day: :tomorrow %> - <%= render "day_tab", forecast: @forecasts.third, day: :day_after_tomorrow %> + <%= render(DayTabComponent.new(forecast: @forecasts.first, day: :today)) %> + <%= render(DayTabComponent.new(forecast: @forecasts.second, day: :tomorrow)) %> + <%= render(DayTabComponent.new(forecast: @forecasts.third, day: :day_after_tomorrow)) %> + <%= render partial: "alert_guidance", locals: {air_pollution_prediction: @forecasts.first} %> <%= render partial: "map_selector" %>
diff --git a/app/views/styled_forecasts/update.turbo_stream.erb b/app/views/styled_forecasts/update.turbo_stream.erb new file mode 100644 index 00000000..8fdf4101 --- /dev/null +++ b/app/views/styled_forecasts/update.turbo_stream.erb @@ -0,0 +1,2 @@ +<%= turbo_stream.replace("day_predictions", partial: "predictions", locals: {forecast: @day_forecast}) %> +<%= turbo_stream.replace("alert_guidance", partial: "alert_guidance", locals: {air_pollution_prediction: @day_forecast}) %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 0030e4e7..4cb6ad28 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -54,28 +54,28 @@ en: title: Action may be required detail_html: Adults and children with lung problems, and adults with heart - problems, who experience symptoms, should think about reducing + problems, who experience symptoms, should think about reducing strenuous physical activity, particularly outdoors. high: guidance: title: Action required detail_html: - Think about reducing activity, particularly outdoors, if you - have discomfort such as sore eyes, a cough or sore - throat.

Older people, adults and children with lung problems, - and adults with heart problems should reduce strenuous physical - activity, particularly outdoors, and especially if they experience - symptoms.

People with asthma may need to use their reliever - inhaler more often, but remember to never exceed the stated dose or - take more than your doctor has advised + Think about reducing activity, particularly outdoors, if you have + discomfort such as sore eyes, a cough or sore throat.

Older + people, adults and children with lung problems, and adults with heart + problems should reduce strenuous physical activity, particularly + outdoors, and especially if they experience symptoms.

People + with asthma may need to use their reliever inhaler more often, but + remember to never exceed the stated dose or take more than your doctor + has advised. very_high: guidance: title: Action required detail_html: - Everyone should reduce strenuous physical activity, - particularly outdoors and especially if you experience symptoms such - as a cough or sore throat.

Older people, adults and children - with lung problems, and adults with heart problems should avoid - strenuous physical activity.

People with asthma may need to - use their reliever inhaler more often, but remember to never exceed - the stated dose or take more than your doctor has advised. + Everyone should reduce strenuous physical activity, particularly + outdoors and especially if you experience symptoms such as a cough or + sore throat.

Older people, adults and children with lung + problems, and adults with heart problems should avoid strenuous + physical activity.

People with asthma may need to use their + reliever inhaler more often, but remember to never exceed the stated + dose or take more than your doctor has advised. diff --git a/spec/components/day_tab_component_spec.rb b/spec/components/day_tab_component_spec.rb new file mode 100644 index 00000000..278d2ba6 --- /dev/null +++ b/spec/components/day_tab_component_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe DayTabComponent, type: :component do + describe "daqi_indicator_colour_class" do + context "when the air pollution level is 1" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_1), day: :today) + } + + it "returns _bg-lime-400_" do + expect(component.daqi_indicator_colour_class).to eq("bg-lime-400") + end + end + + context "when the air pollution level is 2" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_2), day: :today) + } + + it "returns _bg-green-400_" do + expect(component.daqi_indicator_colour_class).to eq("bg-green-400") + end + end + + context "when the air pollution level is 3" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_3), day: :today) + } + + it "returns _bg-lime-600_" do + expect(component.daqi_indicator_colour_class).to eq("bg-lime-600") + end + end + + context "when the air pollution level is 4" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_4), day: :today) + } + + it "returns _bg-yellow-300_" do + expect(component.daqi_indicator_colour_class).to eq("bg-yellow-300") + end + end + + context "when the air pollution level is 5" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_5), day: :today) + } + + it "returns _bg-amber-200_" do + expect(component.daqi_indicator_colour_class).to eq("bg-amber-200") + end + end + + context "when the air pollution level is 6" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_6), day: :today) + } + + it "returns _bg-yellow-500_" do + expect(component.daqi_indicator_colour_class).to eq("bg-yellow-500") + end + end + + context "when the air pollution level is 7" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_7), day: :today) + } + + it "returns _bg-orange-500_" do + expect(component.daqi_indicator_colour_class).to eq("bg-orange-500") + end + end + + context "when the air pollution level is 8" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_8), day: :today) + } + + it "returns _bg-red-500_" do + expect(component.daqi_indicator_colour_class).to eq("bg-red-500") + end + end + + context "when the air pollution level is 9" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_9), day: :today) + } + + it "returns _bg-red-800_" do + expect(component.daqi_indicator_colour_class).to eq("bg-red-800") + end + end + + context "when the air pollution level is 10" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_10), day: :today) + } + + it "returns _bg-stone-700_" do + expect(component.daqi_indicator_colour_class).to eq("bg-stone-700") + end + end + end + + describe "classes" do + context "when the day is today" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_1), day: :today) + } + + it "returns the classes for today's tab" do + expect(component.classes).to eq("active daqi-level-1-today") + end + end + + context "when the day is not today" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_1), day: :tomorrow) + } + + it "returns the classes for the after today tabs" do + expect(component.classes).to eq("inactive after-today") + end + end + end +end diff --git a/spec/controllers/styled_forecasts_controller_spec.rb b/spec/controllers/styled_forecasts_controller_spec.rb index 39b0079e..347ce173 100644 --- a/spec/controllers/styled_forecasts_controller_spec.rb +++ b/spec/controllers/styled_forecasts_controller_spec.rb @@ -49,58 +49,14 @@ end describe "GET :update" do - let(:tag_builder) do - instance_double(Turbo::Streams::TagBuilder, replace: true) - end - - before do - allow(Turbo::Streams::TagBuilder).to receive(:new).and_return(tag_builder) - end - context "when a recognised _day_ parameter is received" do - describe "when the day is _today_" do - it "passes the first forecast to the view" do - allow(CercForecastService).to receive(:latest_forecasts_for).and_return(forecasts) - - get :update, params: {day: :today} - - expect(CercForecastService).to have_received(:latest_forecasts_for).with(southwark) - expect(tag_builder).to have_received(:replace).with( - "day_predictions", - partial: "predictions", - locals: {forecast: forecasts.data.first} - ) - end - end - - describe "when the day is _tomorrow_" do - it "passes the second forecast to the view" do - allow(CercForecastService).to receive(:latest_forecasts_for).and_return(forecasts) - - get :update, params: {day: :tomorrow} - - expect(CercForecastService).to have_received(:latest_forecasts_for).with(southwark) - expect(tag_builder).to have_received(:replace).with( - "day_predictions", - partial: "predictions", - locals: {forecast: forecasts.data.second} - ) - end - end - - describe "when the day is _day_after_tomorrow_" do - it "passes the third forecast to the view" do - allow(CercForecastService).to receive(:latest_forecasts_for).and_return(forecasts) + it "renders the turbo update template" do + allow(CercForecastService).to receive(:latest_forecasts_for).and_return(forecasts) - get :update, params: {day: :day_after_tomorrow} + get :update, params: {day: :today}, format: :turbo_stream - expect(CercForecastService).to have_received(:latest_forecasts_for).with(southwark) - expect(tag_builder).to have_received(:replace).with( - "day_predictions", - partial: "predictions", - locals: {forecast: forecasts.data.third} - ) - end + expect(CercForecastService).to have_received(:latest_forecasts_for).with(southwark) + expect(response).to render_template("styled_forecasts/update") end end diff --git a/spec/factories/air_pollutions_predictions.rb b/spec/factories/air_pollutions_predictions.rb index d9c49edb..2e4e2063 100644 --- a/spec/factories/air_pollutions_predictions.rb +++ b/spec/factories/air_pollutions_predictions.rb @@ -53,6 +53,96 @@ ozone { 10 } end + trait 1 do + value { 1 } + label { "LOW" } + nitrogen_dioxide { 1 } + particulate_matter_10 { 1 } + particulate_matter_2_5 { 1 } + ozone { 1 } + end + + trait 2 do + value { 2 } + label { "LOW" } + nitrogen_dioxide { 2 } + particulate_matter_10 { 2 } + particulate_matter_2_5 { 2 } + ozone { 2 } + end + + trait 3 do + value { 3 } + label { "LOW" } + nitrogen_dioxide { 3 } + particulate_matter_10 { 3 } + particulate_matter_2_5 { 3 } + ozone { 3 } + end + + trait 4 do + value { 4 } + label { "MODERATE" } + nitrogen_dioxide { 4 } + particulate_matter_10 { 4 } + particulate_matter_2_5 { 4 } + ozone { 4 } + end + + trait 5 do + value { 5 } + label { "MODERATE" } + nitrogen_dioxide { 5 } + particulate_matter_10 { 5 } + particulate_matter_2_5 { 5 } + ozone { 5 } + end + + trait 6 do + value { 6 } + label { "MODERATE" } + nitrogen_dioxide { 6 } + particulate_matter_10 { 6 } + particulate_matter_2_5 { 6 } + ozone { 6 } + end + + trait 7 do + value { 7 } + label { "HIGH" } + nitrogen_dioxide { 7 } + particulate_matter_10 { 7 } + particulate_matter_2_5 { 7 } + ozone { 7 } + end + + trait 8 do + value { 8 } + label { "HIGH" } + nitrogen_dioxide { 8 } + particulate_matter_10 { 8 } + particulate_matter_2_5 { 8 } + ozone { 8 } + end + + trait 9 do + value { 9 } + label { "HIGH" } + nitrogen_dioxide { 9 } + particulate_matter_10 { 9 } + particulate_matter_2_5 { 9 } + ozone { 9 } + end + + trait 10 do + value { 10 } + label { "VERY HIGH" } + nitrogen_dioxide { 10 } + particulate_matter_10 { 10 } + particulate_matter_2_5 { 10 } + ozone { 10 } + end + initialize_with { new( forecasted_at: forecasted_at, diff --git a/spec/factories/forecasts.rb b/spec/factories/forecasts.rb index f5e94ece..20099aa1 100644 --- a/spec/factories/forecasts.rb +++ b/spec/factories/forecasts.rb @@ -29,6 +29,46 @@ pollen { FactoryBot.build(:pollen_prediction) } temperature { FactoryBot.build(:temperature_prediction) } + trait :air_pollution_level_1 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 1) } + end + + trait :air_pollution_level_2 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 2) } + end + + trait :air_pollution_level_3 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 3) } + end + + trait :air_pollution_level_4 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 4) } + end + + trait :air_pollution_level_5 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 5) } + end + + trait :air_pollution_level_6 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 6) } + end + + trait :air_pollution_level_7 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 7) } + end + + trait :air_pollution_level_8 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 8) } + end + + trait :air_pollution_level_9 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 9) } + end + + trait :air_pollution_level_10 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 10) } + end + initialize_with { new( obtained_at: obtained_at, diff --git a/spec/feature_steps/air_quality_steps.rb b/spec/feature_steps/air_quality_steps.rb index ad9d4283..26ca4930 100644 --- a/spec/feature_steps/air_quality_steps.rb +++ b/spec/feature_steps/air_quality_steps.rb @@ -4,11 +4,11 @@ def given_an_air_pollution_prediction_for_today_w_high_warning_status end def and_an_air_pollution_prediction_for_tomorrow_w_moderate_warning_status - forecasts << Fixtures::API.build_forecast(day: :tomorrow, air_pollution_status: :moderate) + forecasts << Fixtures::API.build_forecast(day: :tomorrow, air_pollution_status: :moderate, daqi_value: 4) end def and_an_air_pollution_prediction_for_day_after_tomorrow_w_v_high_warning_status - forecasts << Fixtures::API.build_forecast(day: :day_after_tomorrow, air_pollution_status: :very_high) + forecasts << Fixtures::API.build_forecast(day: :day_after_tomorrow, air_pollution_status: :very_high, daqi_value: 10) end def given_an_air_pollution_prediction_for_today_w_low_status @@ -28,6 +28,11 @@ def when_i_look_at_the_forecasts click_link("View forecasts") end + def when_i_look_at_the_forecasts_v2 + visit root_path + click_link("View new style forecasts") + end + def expect_to_see_alert_date_for(day) date = case day when :today @@ -41,6 +46,19 @@ def expect_to_see_alert_date_for(day) expect(page).to have_content("air quality alert for #{date}") end + def expect_to_see_alert_date_for_v2(day) + date = case day + when :today + Date.today.strftime("%d %b %Y") + when :tomorrow + Date.tomorrow.strftime("%d %b %Y") + when :day_after_tomorrow + (Date.tomorrow + 1.day).strftime("%d %b %Y") + end + + expect(page).to have_content("air quality alert for #{date}") + end + def expect_to_see_alert_level(level) label = case level when :moderate @@ -54,12 +72,23 @@ def expect_to_see_alert_level(level) expect(page).to have_css(".alert-level", text: label) end + def expect_to_see_alert_level_v2(level) + label = case level + when :moderate + "Moderate" + when :high + "High" + when :very_high + "Very high" + end + + expect(page).to have_css(".daqi-label", text: label) + end + def expect_to_see_guidance_for(level) expect(page).to have_content(I18n.t("air_quality_alert.#{level}.guidance.title")) expect(page).to have_content( - ActionController::Base.helpers.strip_tags( - I18n.t("air_quality_alert.#{level}.guidance.detail_html") - ) + I18n.t("air_quality_alert.#{level}.guidance.detail_html").truncate(20, omission: "") ) end @@ -71,6 +100,39 @@ def then_i_see_an_air_quality_alert_of_high_for_today end end + def then_i_see_an_air_quality_alert_of_high_for_today_v2 + within(".today[data-date='#{Date.today}']") do + expect_to_see_alert_level_v2(:high) + end + within(".alert-guidance") do + expect_to_see_guidance_for(:high) + end + end + + def then_i_see_an_air_quality_alert_of_moderate_for_tomorrow_v2 + within(".tomorrow[data-date='#{Date.tomorrow}']") do + expect_to_see_alert_level_v2(:moderate) + end + within(".alert-guidance") do + expect_to_see_guidance_for(:moderate) + end + + expect(page).to have_css(".tab.tomorrow.daqi-alert-after-today-selected-level-4") + expect(page).not_to have_css(".tab.day_after_tomorrow.daqi-alert-after-today-selected-level-10") + end + + def then_i_see_an_air_quality_alert_of_v_high_for_day_after_tomorrow_v2 + within(".day_after_tomorrow[data-date='#{Date.tomorrow + 1.day}']") do + expect_to_see_alert_level_v2(:very_high) + end + within(".alert-guidance") do + expect_to_see_guidance_for(:very_high) + end + + expect(page).to have_css(".tab.day_after_tomorrow.daqi-alert-after-today-selected-level-10") + expect(page).not_to have_css(".tab.tomorrow.daqi-alert-after-today-selected-level-4") + end + def and_i_see_an_air_quality_alert_of_moderate_for_tomorrow within(".alert-level-moderate[data-alert-date='#{Date.tomorrow}']") do expect_to_see_alert_date_for(:tomorrow) diff --git a/spec/feature_steps/forecast_steps.rb b/spec/feature_steps/forecast_steps.rb index f61113dc..30727d82 100644 --- a/spec/feature_steps/forecast_steps.rb +++ b/spec/feature_steps/forecast_steps.rb @@ -57,9 +57,9 @@ def and_i_switch_to_the_tab_for_day_after_tomorrow def switch_to_tab_for(day) case day when :tomorrow - find(".tab.tomorrow a").click + find(".tab.tomorrow a").trigger("click") when :day_after_tomorrow - find(".tab.day_after_tomorrow a").click + find(".tab.day_after_tomorrow a").trigger("click") else raise "day: #{day} not expected" end @@ -114,6 +114,9 @@ def and_i_see_predicted_uv_level_v2 def then_i_see_that_the_tomorrow_tab_is_active expect(page).to have_css(".tab.tomorrow.active") + expect(page).to have_css(".tab.today.inactive") + expect(page).to have_css(".tab.day_after_tomorrow.inactive") + expect(page).not_to have_css(".tab.today.active") expect(page).not_to have_css(".tab.day_after_tomorrow.active") end @@ -121,6 +124,9 @@ def then_i_see_that_the_tomorrow_tab_is_active def and_i_see_that_the_today_tab_is_active expect(page).to have_css(".tab.today.active") + expect(page).to have_css(".tab.tomorrow.inactive") + expect(page).to have_css(".tab.day_after_tomorrow.inactive") + expect(page).not_to have_css(".tab.tomorrow.active") expect(page).not_to have_css(".tab.day_after_tomorrow.active") end @@ -128,6 +134,9 @@ def and_i_see_that_the_today_tab_is_active def then_i_see_that_the_day_after_tomorrow_tab_is_active expect(page).to have_css(".tab.day_after_tomorrow.active") + expect(page).to have_css(".tab.today.inactive") + expect(page).to have_css(".tab.tomorrow.inactive") + expect(page).not_to have_css(".tab.today.active") expect(page).not_to have_css(".tab.tomorrow.active") end diff --git a/spec/features/visitors/view_styled_air_quality_alerts_spec.rb b/spec/features/visitors/view_styled_air_quality_alerts_spec.rb new file mode 100644 index 00000000..f6e9ef8f --- /dev/null +++ b/spec/features/visitors/view_styled_air_quality_alerts_spec.rb @@ -0,0 +1,46 @@ +# Feature: View air quality alerts +# - So that I can take appropriate protective action +# - As a visitor +# - I want to see Air quality alerts for air pollution predictions with warning +# statuses above "low" +RSpec.feature "Air quality alerts", feature: true do + around do |example| + env_vars = { + CERC_API_HOST_URL: "https://cerc.example.com", + CERC_API_KEY: "SECRET-API-KEY", + CERC_API_CACHE_LIMIT_MINS: "60", + MAPTILER_API_KEY: "TOPSECRET" + } + ClimateControl.modify(env_vars) { example.run } + end + + include AirQualitySteps, ForecastSteps + + before do + given_an_air_pollution_prediction_for_today_w_high_warning_status + and_an_air_pollution_prediction_for_tomorrow_w_moderate_warning_status + and_an_air_pollution_prediction_for_day_after_tomorrow_w_v_high_warning_status + and_the_response_from_cercs_api_is_stubbed_accordingly + end + + scenario "View air quality alert for today" do + when_i_look_at_the_forecasts_v2 + then_i_see_an_air_quality_alert_of_high_for_today_v2 + end + + scenario "View air quality alert for tomorrow", js: true do + visit root_path + when_i_select_view_forecasts_v2 + and_i_switch_to_the_tab_for_tomorrow + + then_i_see_an_air_quality_alert_of_moderate_for_tomorrow_v2 + end + + scenario "View air quality alert for the day after tomorrow", js: true do + visit root_path + when_i_select_view_forecasts_v2 + and_i_switch_to_the_tab_for_day_after_tomorrow + + then_i_see_an_air_quality_alert_of_v_high_for_day_after_tomorrow_v2 + end +end diff --git a/spec/features/visitors/view_styled_forecasts_spec.rb b/spec/features/visitors/view_styled_forecasts_spec.rb index fc6c0fb6..3d0fdee8 100644 --- a/spec/features/visitors/view_styled_forecasts_spec.rb +++ b/spec/features/visitors/view_styled_forecasts_spec.rb @@ -58,8 +58,8 @@ visit root_path when_i_select_view_forecasts_v2 - and_i_switch_to_the_tab_for_day_after_tomorrow + and_i_switch_to_the_tab_for_day_after_tomorrow then_i_see_that_the_day_after_tomorrow_tab_is_active and_i_see_predicted_uv_level_for_day_after_tomorrow and_i_see_predicted_pollen_level_for_day_after_tomorrow diff --git a/spec/fixtures/api/forecasts.rb b/spec/fixtures/api/forecasts.rb index b6ba4eb3..69fd2a57 100644 --- a/spec/fixtures/api/forecasts.rb +++ b/spec/fixtures/api/forecasts.rb @@ -1,7 +1,7 @@ module Fixtures module API class << self - def build_forecast(day:, air_pollution_status:, pollen: :moderate, temperature: :normal, uv: :moderate) + def build_forecast(day:, air_pollution_status:, pollen: :moderate, temperature: :normal, uv: :moderate, daqi_value: nil) <<~JSON { "NO2": 1, @@ -16,7 +16,7 @@ def build_forecast(day:, air_pollution_status:, pollen: :moderate, temperature: "rain_pm": 3.01, "temp_max": #{max_temp_for(temperature)}, "temp_min": #{min_temp_for(temperature)}, - "total": #{daqi_value_for_level(air_pollution_status)}, + "total": #{daqi_value || daqi_value_for_level(air_pollution_status)}, "total_status": "#{total_status_for(air_pollution_status)}", "uv": #{daqi_value_for_level(uv)}, "wind_am": 5.3, diff --git a/tailwind.config.js b/tailwind.config.js index 4e478437..c46cc364 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -5,5 +5,39 @@ module.exports = { './app/assets/stylesheets/**/*.css', './app/javascript/**/*.js', './app/components/**/*.html.erb' - ] + ], + theme: { + extend: { + colors: { + 'moderate-alert-guidance-panel': '#FBDA3033', + 'high-alert-guidance-panel': '#EBDBDE', + 'very-high-alert-guidance-panel': '#EBDBDE', + }, + } + }, + safelist: [ + 'bg-moderate-alert-guidance-panel', + 'bg-high-alert-guidance-panel', + 'bg-very-high-alert-guidance-panel', + "bg-lime-400", + "bg-green-400", + "bg-lime-600", + "bg-yellow-300", + "bg-amber-200", + "bg-yellow-500", + "bg-orange-500", + "bg-red-500", + "bg-red-800", + "bg-stone-700", + "text-lime-400", + "text-green-400", + "text-lime-600", + "text-yellow-300", + "text-amber-200", + "text-yellow-500", + "text-orange-500", + "text-red-500", + "text-red-800", + "text-stone-700" + ], }