Skip to content

Commit

Permalink
add tests for behavior within the modal
Browse files Browse the repository at this point in the history
  • Loading branch information
klaustopher committed Jan 15, 2025
1 parent fbdbecf commit 62b5506
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 15 deletions.
15 changes: 10 additions & 5 deletions frontend/src/stimulus/controllers/dynamic/time-entry.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ export default class TimeEntryController extends Controller {
this.datesChanged(event.currentTarget as HTMLInputElement);
}

parsedHourInput():number {
const normalizedValue = this.hoursInputTarget.value.replace(',', '.');
return parseChronicDuration(normalizedValue, {
defaultUnit: 'hours',
ignoreSecondsWhenColonSeperated: true,
}) || 0;
}

datesChanged(initiatedBy:HTMLInputElement) {
if (!this.hasStartTimeInputTarget || !this.hasEndTimeInputTarget) {
return;
Expand All @@ -104,7 +112,7 @@ export default class TimeEntryController extends Controller {

const startTimeInMinutes = parseInt(startTimeParts[0], 10) * 60 + parseInt(startTimeParts[1], 10);
const endTimeInMinutes = parseInt(endTimeParts[0], 10) * 60 + parseInt(endTimeParts[1], 10);
let hoursInMinutes = Math.round((parseChronicDuration(this.hoursInputTarget.value) || 0) / 60);
let hoursInMinutes = Math.round(this.parsedHourInput() / 60);

// We calculate the hours field if:
// - We have start & end time and no hours
Expand Down Expand Up @@ -141,10 +149,7 @@ export default class TimeEntryController extends Controller {
hoursChanged() {
// Parse input through our chronic duration parser and then reformat as hours that can be nicely parsed on the
// backend
const hours = parseChronicDuration(this.hoursInputTarget.value, {
defaultUnit: 'hours',
ignoreSecondsWhenColonSeperated: true,
});
const hours = this.parsedHourInput();
this.hoursInputTarget.value = outputChronicDuration(hours, { format: 'hours_only' }) || '';

this.datesChanged(this.hoursInputTarget);
Expand Down
140 changes: 131 additions & 9 deletions modules/costs/spec/features/time_entry_dialog_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@
shared_let(:work_package_a) { create(:work_package, subject: "WP A", project:) }
shared_let(:work_package_b) { create(:work_package, subject: "WP B", project:) }

let(:wp_view_a) { Pages::FullWorkPackage.new(work_package_a) }
let(:wp_view_b) { Pages::FullWorkPackage.new(work_package_b) }
let(:time_logging_modal) { Components::TimeLoggingModal.new }

let(:user) { create(:user, member_with_permissions: { project => permissions }) }
Expand All @@ -49,28 +47,152 @@
context "when user has permission to log own time" do
let(:permissions) { %i[log_own_time view_own_time_entries view_work_packages] }

before do
visit work_package_path(work_package_a)

find("#action-show-more-dropdown-menu .button").click
find(".menu-item", text: "Log time").click
end

it "does not show the user autocompleter" do
time_logging_modal.is_visible(true)
time_logging_modal.shows_field("user_id", false)
end

context "when start and end time is not allowed", with_settings: { allow_tracking_start_and_end_times: false } do
it "does not show fields to track start and end times" do
time_logging_modal.shows_field("start_time", false)
time_logging_modal.shows_field("end_time", false)
time_logging_modal.shows_field("hours", true)
end
end

context "when start and end time is allowed", with_settings: { allow_tracking_start_and_end_times: true } do
it "shows fields to track start and end times" do
time_logging_modal.shows_field("start_time", true)
time_logging_modal.requires_field("start_time", required: false)
time_logging_modal.shows_field("end_time", true)
time_logging_modal.requires_field("end_time", required: false)
time_logging_modal.shows_field("hours", true)
end
end

context "when start and end time is enforced",
with_settings: { allow_tracking_start_and_end_times: true, enforce_tracking_start_and_end_times: true } do
it "shows fields to track start and end times" do
time_logging_modal.shows_field("start_time", true)
time_logging_modal.requires_field("start_time")
time_logging_modal.shows_field("end_time", true)
time_logging_modal.requires_field("end_time")
time_logging_modal.shows_field("hours", true)
end
end
end

context "when user has permission to log time for others" do
let!(:other_user) { create(:user, member_with_permissions: { project => [:view_project] }) }
let!(:other_user) do
create(
:user,
firstname: "Max",
lastname: "Mustermann",
preferences: {
time_zone: "Asia/Tokyo"
},
member_with_permissions: { project => [:view_project] }
)
end
let(:permissions) { %i[log_time view_time_entries view_work_packages] }
end

context "when user has permission to edit own time entries" do
let(:permissions) { %i[log_own_time view_own_time_entries edit_own_time_entries view_work_packages] }
before do
visit work_package_path(work_package_a)

find("#action-show-more-dropdown-menu .button").click
find(".menu-item", text: "Log time").click
end

it "shows the user autocompleter and prefills it with the current user" do
time_logging_modal.is_visible(true)
time_logging_modal.shows_field("user_id", true)
time_logging_modal.expect_user(user)

time_logging_modal.update_field("user_id", other_user.name)

time_logging_modal.expect_user(other_user)
time_logging_modal.shows_caption(I18n.t("notice_different_time_zones", tz: "(UTC+09:00) Osaka"))
end
end

context "when user has permission to edit time entries for others" do
let!(:other_user) { create(:user, member_with_permissions: { project => [:view_project] }) }
let(:permissions) { %i[log_time view_time_entries edit_time_entries view_work_packages] }
describe "calculating logic", with_settings: { allow_tracking_start_and_end_times: true } do
let(:permissions) { %i[log_own_time view_own_time_entries view_work_packages] }

before do
visit work_package_path(work_package_a)

find("#action-show-more-dropdown-menu .button").click
find(".menu-item", text: "Log time").click
end

it "normalizes the hour input" do
time_logging_modal.update_field("hours", "6h 45min")
time_logging_modal.has_field_with_value("hours", "6.75h")

time_logging_modal.update_field("hours", "4:15")
time_logging_modal.has_field_with_value("hours", "4.25h")

time_logging_modal.update_field("hours", "1m 2w 3d 4h 5m")
time_logging_modal.has_field_with_value("hours", "412.1h")

time_logging_modal.update_field("hours", "1.5")
time_logging_modal.has_field_with_value("hours", "1.5h")

time_logging_modal.update_field("hours", "3,7")
time_logging_modal.has_field_with_value("hours", "3.7h")
end

it "calculates the hours based on the start and end time" do
time_logging_modal.update_time_field("start_time", hour: 10, minute: 0)
time_logging_modal.update_time_field("end_time", hour: 12, minute: 30)

time_logging_modal.has_field_with_value("hours", "2.5h")
end

it "correctly handles when end_time < start_time (multiple days)" do
time_logging_modal.update_time_field("start_time", hour: 10, minute: 0)
time_logging_modal.update_time_field("end_time", hour: 9, minute: 45)

time_logging_modal.has_field_with_value("hours", "23.75h")
time_logging_modal.shows_caption("+1 day")
end

it "correctly handles when hours > 24" do
time_logging_modal.update_time_field("start_time", hour: 10, minute: 0)
time_logging_modal.update_field("hours", "50h")

time_logging_modal.has_field_with_value("end_time", "12:00")
time_logging_modal.shows_caption("+2 days")
end

it "recalculates the end time, when changing the hours field" do
time_logging_modal.update_time_field("start_time", hour: 10, minute: 0)
time_logging_modal.update_time_field("end_time", hour: 12, minute: 30)

time_logging_modal.has_field_with_value("hours", "2.5h")

time_logging_modal.update_field("hours", "6h")

time_logging_modal.has_field_with_value("end_time", "16:00")
end

it "recalculates the end time, when changing the start_time field" do
time_logging_modal.update_time_field("start_time", hour: 10, minute: 0)
time_logging_modal.update_time_field("end_time", hour: 12, minute: 30)

time_logging_modal.has_field_with_value("hours", "2.5h")

time_logging_modal.update_time_field("start_time", hour: 12, minute: 0)

time_logging_modal.has_field_with_value("end_time", "14:30")
time_logging_modal.has_field_with_value("hours", "2.5h")
end
end
end
40 changes: 39 additions & 1 deletion spec/features/support/components/time_logging_modal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,36 @@ def has_field_with_value(field, value)

def expect_work_package(subject)
within modal_container do
expect(page).to have_css(".ng-value", text: subject, wait: 10)
expect(page).to have_css("opce-user-autocompleter > .ng-value", text: subject, wait: 10)
end
end

def expect_user(user)
within modal_container do
expect(page).to have_css(".ng-value", text: user.name, wait: 10)
end
end

def shows_caption(caption)
within modal_container do
expect(page).to have_css(".FormControl-caption", text: caption)
end
end

def requires_field(field, required: true)
within modal_container do
if required
expect(page).to have_css "#time_entry_#{field}[aria-required=true]"
else
expect(page).to have_no_css "#time_entry_#{field}[aria-required=true]"
end
end
end

def trigger_field_change_event(field)
modal_container.find("#time_entry_#{field}").trigger("change")
end

def shows_field(field, visible)
within modal_container do
if visible
Expand All @@ -91,6 +117,11 @@ def shows_field(field, visible)
end
end

def update_time_field(field_name, hour:, minute:)
built_time = browser_timezone.local(2025, 1, 1, hour, minute, 0)
page.fill_in "time_entry_#{field_name}", with: built_time.iso8601
end

def update_field(field_name, value)
if field_name.in?(["work_package_id", "user_id", "activity_id"])
select_autocomplete modal_container.find("#time_entry_#{field_name}"),
Expand Down Expand Up @@ -144,6 +175,13 @@ def has_hidden_work_package_field_for(work_package)

private

def browser_timezone
return @browser_timezone if defined?(@browser_timezone)

browser_tz_name = page.evaluate_script("Intl.DateTimeFormat().resolvedOptions().timeZone")
@browser_timezone = ActiveSupport::TimeZone[browser_tz_name]
end

def modal_container
page.find("dialog#time-entry-dialog", visible: :all)
end
Expand Down

0 comments on commit 62b5506

Please sign in to comment.