Commit 55f36fa5 authored by Peter Leitzen's avatar Peter Leitzen

Merge branch '262847-add-schedule' into 'master'

Add a schedule modal

See merge request gitlab-org/gitlab!48122
parents 8df40386 21b672cd
@import 'mixins_and_variables_and_functions';
@mixin inset-border-1-red-500($important: false) {
box-shadow: inset 0 0 0 $gl-border-size-1 $red-500 if-important($important);
}
.timezone-dropdown {
.dropdown-menu {
@include gl-w-full;
}
.gl-new-dropdown-item-text-primary {
@include gl-overflow-hidden;
@include gl-text-overflow-ellipsis;
}
}
.modal-footer {
@include gl-bg-gray-10;
}
.invalid-dropdown {
.gl-dropdown-toggle {
@include inset-border-1-red-500;
&:hover {
@include inset-border-1-red-500(true);
}
}
}
# frozen_string_literal: true
module Ci
module PipelineSchedulesHelper
def timezone_data
ActiveSupport::TimeZone.all.map do |timezone|
{
name: timezone.name,
offset: timezone.now.utc_offset,
identifier: timezone.tzinfo.identifier
}
end
end
end
end
# frozen_string_literal: true
module TimeZoneHelper
def timezone_data
ActiveSupport::TimeZone.all.map do |timezone|
{
identifier: timezone.tzinfo.identifier,
name: timezone.name,
abbr: timezone.tzinfo.strftime('%Z'),
offset: timezone.now.utc_offset,
formatted_offset: timezone.now.formatted_offset
}
end
end
end
...@@ -203,6 +203,7 @@ module Gitlab ...@@ -203,6 +203,7 @@ module Gitlab
config.assets.precompile << "page_bundles/wiki.css" config.assets.precompile << "page_bundles/wiki.css"
config.assets.precompile << "page_bundles/xterm.css" config.assets.precompile << "page_bundles/xterm.css"
config.assets.precompile << "page_bundles/alert_management_settings.css" config.assets.precompile << "page_bundles/alert_management_settings.css"
config.assets.precompile << "page_bundles/oncall_schedules.css"
config.assets.precompile << "lazy_bundles/cropper.css" config.assets.precompile << "lazy_bundles/cropper.css"
config.assets.precompile << "lazy_bundles/select2.css" config.assets.precompile << "lazy_bundles/select2.css"
config.assets.precompile << "performance_bar.css" config.assets.precompile << "performance_bar.css"
......
<script>
import { isEqual, isEmpty } from 'lodash';
import {
GlModal,
GlForm,
GlFormGroup,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlAlert,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import createOncallScheduleMutation from '../graphql/create_oncall_schedule.mutation.graphql';
export const i18n = {
selectTimezone: s__('OnCallSchedules|Select timezone'),
search: __('Search'),
noResults: __('No matching results'),
cancel: __('Cancel'),
addSchedule: s__('OnCallSchedules|Add schedule'),
fields: {
name: {
title: __('Name'),
validation: {
empty: __("Can't be empty"),
},
},
description: { title: __('Description (optional)') },
timezone: {
title: __('Timezone'),
description: s__(
'OnCallSchedules|Sets the default timezone for the schedule, for all participants',
),
validation: {
empty: __("Can't be empty"),
},
},
},
errorMsg: s__('OnCallSchedules|Failed to add schedule'),
};
export default {
i18n,
inject: ['projectPath', 'timezones'],
components: {
GlModal,
GlForm,
GlFormGroup,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlSearchBoxByType,
GlAlert,
},
props: {
modalId: {
type: String,
required: true,
},
},
data() {
return {
loading: false,
tzSearchTerm: '',
form: {
name: '',
description: '',
timezone: {},
},
error: null,
};
},
computed: {
actionsProps() {
return {
primary: {
text: i18n.addSchedule,
attributes: [
{ variant: 'info' },
{ loading: this.loading },
{ disabled: this.isFormInvalid },
],
},
cancel: {
text: i18n.cancel,
},
};
},
filteredTimezones() {
const lowerCaseTzSearchTerm = this.tzSearchTerm.toLowerCase();
return this.timezones.filter(tz =>
this.getFormattedTimezone(tz)
.toLowerCase()
.includes(lowerCaseTzSearchTerm),
);
},
noResults() {
return !this.filteredTimezones.length;
},
selectedTimezone() {
return isEmpty(this.form.timezone)
? i18n.selectTimezone
: this.getFormattedTimezone(this.form.timezone);
},
isNameInvalid() {
return !this.form.name.length;
},
isTimezoneInvalid() {
return isEmpty(this.form.timezone);
},
isFormInvalid() {
return this.isNameInvalid || this.isTimezoneInvalid;
},
},
methods: {
createSchedule() {
this.loading = true;
this.$apollo
.mutate({
mutation: createOncallScheduleMutation,
variables: {
oncallScheduleCreateInput: {
projectPath: this.projectPath,
...this.form,
timezone: this.form.timezone.identifier,
},
},
})
.then(({ data: { oncallScheduleCreate: { errors: [error] } } }) => {
if (error) {
throw error;
}
this.$refs.createScheduleModal.hide();
})
.catch(error => {
this.error = error;
})
.finally(() => {
this.loading = false;
});
},
setSelectedTimezone(tz) {
this.form.timezone = tz;
},
getFormattedTimezone(tz) {
return __(`(UTC${tz.formatted_offset}) ${tz.abbr} ${tz.name}`);
},
isTimezoneSelected(tz) {
return isEqual(tz, this.form.timezone);
},
hideErrorAlert() {
this.error = null;
},
},
};
</script>
<template>
<gl-modal
ref="createScheduleModal"
:modal-id="modalId"
size="sm"
:title="$options.i18n.addSchedule"
:action-primary="actionsProps.primary"
:action-cancel="actionsProps.cancel"
@primary.prevent="createSchedule"
>
<gl-alert v-if="error" variant="danger" class="gl-mt-n3 gl-mb-3" @dismiss="hideErrorAlert">
{{ error || $options.i18n.errorMsg }}
</gl-alert>
<gl-form>
<gl-form-group
:label="$options.i18n.fields.name.title"
:invalid-feedback="$options.i18n.fields.name.validation.empty"
label-size="sm"
label-for="schedule-name"
>
<gl-form-input id="schedule-name" v-model="form.name" :state="!isNameInvalid" />
</gl-form-group>
<gl-form-group
:label="$options.i18n.fields.description.title"
label-size="sm"
label-for="schedule-description"
>
<gl-form-input id="schedule-description" v-model="form.description" />
</gl-form-group>
<gl-form-group
:label="$options.i18n.fields.timezone.title"
label-size="sm"
label-for="schedule-timezone"
:description="$options.i18n.fields.timezone.description"
:state="!isTimezoneInvalid"
:invalid-feedback="$options.i18n.fields.timezone.validation.empty"
>
<gl-dropdown
id="schedule-timezone"
:text="selectedTimezone"
class="timezone-dropdown gl-w-full"
:header-text="$options.i18n.selectTimezone"
:class="{ 'invalid-dropdown': isTimezoneInvalid }"
>
<gl-search-box-by-type v-model.trim="tzSearchTerm" />
<gl-dropdown-item
v-for="tz in filteredTimezones"
:key="getFormattedTimezone(tz)"
:is-checked="isTimezoneSelected(tz)"
is-check-item
@click="setSelectedTimezone(tz)"
>
<span class="gl-white-space-nowrap"> {{ getFormattedTimezone(tz) }}</span>
</gl-dropdown-item>
<gl-dropdown-item v-if="noResults">
{{ $options.i18n.noResults }}
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
</gl-form>
</gl-modal>
</template>
<script> <script>
import { GlEmptyState, GlButton } from '@gitlab/ui'; import { GlEmptyState, GlButton, GlModalDirective } from '@gitlab/ui';
import AddScheduleModal from './add_schedule_modal.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
const addScheduleModalId = 'addScheduleModal';
export const i18n = { export const i18n = {
emptyState: { emptyState: {
title: s__('OnCallSchedules|Create on-call schedules in GitLab'), title: s__('OnCallSchedules|Create on-call schedules in GitLab'),
...@@ -12,27 +15,33 @@ export const i18n = { ...@@ -12,27 +15,33 @@ export const i18n = {
export default { export default {
i18n, i18n,
addScheduleModalId,
inject: ['emptyOncallSchedulesSvgPath'], inject: ['emptyOncallSchedulesSvgPath'],
components: { components: {
GlEmptyState, GlEmptyState,
GlButton, GlButton,
AddScheduleModal,
}, },
methods: { directives: {
createSchedule() {}, GlModal: GlModalDirective,
}, },
methods: {},
}; };
</script> </script>
<template> <template>
<div>
<gl-empty-state <gl-empty-state
:title="$options.i18n.emptyState.title" :title="$options.i18n.emptyState.title"
:description="$options.i18n.emptyState.description" :description="$options.i18n.emptyState.description"
:svg-path="emptyOncallSchedulesSvgPath" :svg-path="emptyOncallSchedulesSvgPath"
> >
<template #actions> <template #actions>
<gl-button variant="info" @click="createSchedule">{{ <gl-button v-gl-modal="$options.addScheduleModalId" variant="info">
$options.i18n.emptyState.button {{ $options.i18n.emptyState.button }}
}}</gl-button> </gl-button>
</template> </template>
</gl-empty-state> </gl-empty-state>
<add-schedule-modal :modal-id="$options.addScheduleModalId" />
</div>
</template> </template>
mutation oncallScheduleCreate($oncallScheduleCreateInput: OncallScheduleCreateInput!) {
oncallScheduleCreate(input: $oncallScheduleCreateInput) {
errors
oncallSchedule {
iid
name
description
timezone
}
}
}
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo';
import OnCallSchedulesWrapper from './components/oncall_schedules_wrapper.vue'; import OnCallSchedulesWrapper from './components/oncall_schedules_wrapper.vue';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
export default () => { export default () => {
const el = document.querySelector('#js-oncall_schedule'); const el = document.querySelector('#js-oncall_schedule');
if (!el) return null; if (!el) return null;
const { emptyOncallSchedulesSvgPath } = el.dataset; const { projectPath, emptyOncallSchedulesSvgPath, timezones } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
return new Vue({ return new Vue({
el, el,
apolloProvider,
provide: { provide: {
projectPath,
emptyOncallSchedulesSvgPath, emptyOncallSchedulesSvgPath,
timezones: JSON.parse(timezones),
}, },
render(createElement) { render(createElement) {
return createElement(OnCallSchedulesWrapper); return createElement(OnCallSchedulesWrapper);
......
...@@ -2,9 +2,11 @@ ...@@ -2,9 +2,11 @@
module IncidentManagement module IncidentManagement
module OncallScheduleHelper module OncallScheduleHelper
def oncall_schedule_data def oncall_schedule_data(project)
{ {
'empty-oncall-schedules-svg-path' => image_path('illustrations/empty-state/empty-on-call.svg') 'project-path' => project.full_path,
'empty-oncall-schedules-svg-path' => image_path('illustrations/empty-state/empty-on-call.svg'),
'timezones' => timezone_data.to_json
} }
end end
end end
......
- page_title _('On-call schedules') - page_title _('On-call schedules')
- add_page_specific_style 'page_bundles/oncall_schedules'
#js-oncall_schedule{ data: oncall_schedule_data } #js-oncall_schedule{ data: oncall_schedule_data(@project) }
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AddScheduleModal renders modal layout 1`] = `
<gl-modal-stub
actioncancel="[object Object]"
actionprimary="[object Object]"
modalclass=""
modalid="modalId"
size="sm"
title="Add schedule"
titletag="h4"
>
<!---->
<gl-form-stub>
<gl-form-group-stub
invalid-feedback="Can't be empty"
label="Name"
label-for="schedule-name"
label-size="sm"
>
<gl-form-input-stub
id="schedule-name"
value=""
/>
</gl-form-group-stub>
<gl-form-group-stub
label="Description (optional)"
label-for="schedule-description"
label-size="sm"
>
<gl-form-input-stub
id="schedule-description"
value=""
/>
</gl-form-group-stub>
<gl-form-group-stub
description="Sets the default timezone for the schedule, for all participants"
invalid-feedback="Can't be empty"
label="Timezone"
label-for="schedule-timezone"
label-size="sm"
>
<gl-dropdown-stub
category="primary"
class="timezone-dropdown gl-w-full invalid-dropdown"
headertext="Select timezone"
id="schedule-timezone"
size="medium"
text="Select timezone"
variant="default"
>
<gl-search-box-by-type-stub
clearbuttontitle="Clear"
value=""
/>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-12:00) -12 International Date Line West
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-11:00) SST American Samoa
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-11:00) SST Midway Island
</span>
</gl-dropdown-item-stub>
<gl-dropdown-item-stub
avatarurl=""
iconcolor=""
iconname=""
iconrightarialabel=""
iconrightname=""
ischeckitem="true"
secondarytext=""
>
<span
class="gl-white-space-nowrap"
>
(UTC-10:00) HST Hawaii
</span>
</gl-dropdown-item-stub>
<!---->
</gl-dropdown-stub>
</gl-form-group-stub>
</gl-form-stub>
</gl-modal-stub>
`;
import { shallowMount } from '@vue/test-utils';
import { GlSearchBoxByType, GlDropdown, GlDropdownItem, GlModal, GlAlert } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import AddScheduleModal, { i18n } from 'ee/oncall_schedules/components/add_schedule_modal.vue';
import mockTimezones from './mocks/mockTimezones.json';
describe('AddScheduleModal', () => {
let wrapper;
const projectPath = 'group/project';
const mutate = jest.fn();
const mockHideModal = jest.fn();
function mountComponent() {
wrapper = shallowMount(AddScheduleModal, {
propsData: {
modalId: 'modalId',
},
provide: {
projectPath,
timezones: mockTimezones,
},
mocks: {
$apollo: {
mutate,
},
},
stubs: {
GlFormGroup: false,
},
});
wrapper.vm.$refs.createScheduleModal.hide = mockHideModal;
}
beforeEach(() => {
mountComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findModal = () => wrapper.find(GlModal);
const findAlert = () => wrapper.find(GlAlert);
const findTimezoneDropdown = () => wrapper.find(GlDropdown);
const findDropdownOptions = () => wrapper.findAll(GlDropdownItem);
const findTimezoneSearchBox = () => wrapper.find(GlSearchBoxByType);
it('renders modal layout', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe('Timezone select', () => {
it('has options based on provided BE data', () => {
expect(findDropdownOptions().length).toBe(mockTimezones.length);
});
it('formats each option', () => {
findDropdownOptions().wrappers.forEach((option, index) => {
const tz = mockTimezones[index];
const expectedValue = `(UTC${tz.formatted_offset}) ${tz.abbr} ${tz.name}`;
expect(option.text()).toBe(expectedValue);
});
});
describe('timezones filtering', () => {
it('should filter options based on search term', async () => {
const searchTerm = 'Hawaii';
findTimezoneSearchBox().vm.$emit('input', searchTerm);
await wrapper.vm.$nextTick();
const options = findDropdownOptions();
expect(options.length).toBe(1);
expect(options.at(0).text()).toContain(searchTerm);
});
it('should display no results item when there are no filter matches', async () => {
const searchTerm = 'someUnexistentTZ';
findTimezoneSearchBox().vm.$emit('input', searchTerm);
await wrapper.vm.$nextTick();
const options = findDropdownOptions();
expect(options.length).toBe(1);
expect(options.at(0).text()).toContain(i18n.noResults);
});
});
it('should add a checkmark to the selected option', async () => {
const selectedTZOption = findDropdownOptions().at(0);
selectedTZOption.vm.$emit('click');
await wrapper.vm.$nextTick();
expect(selectedTZOption.attributes('ischecked')).toBe('true');
});
});
describe('Schedule create', () => {
it('makes a request with form data to create a schedule', () => {
mutate.mockResolvedValueOnce({});
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
expect(mutate).toHaveBeenCalledWith({
mutation: expect.any(Object),
variables: { oncallScheduleCreateInput: expect.objectContaining({ projectPath }) },
});
});
it('hides the modal on successful schedule creation', async () => {
mutate.mockResolvedValueOnce({ data: { oncallScheduleCreate: { errors: [] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
expect(mockHideModal).toHaveBeenCalled();
});
it("doesn't hide a modal and shows error alert on fail", async () => {
const error = 'some error';
mutate.mockResolvedValueOnce({ data: { oncallScheduleCreate: { errors: [error] } } });
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
const alert = findAlert();
expect(mockHideModal).not.toHaveBeenCalled();
expect(alert.exists()).toBe(true);
expect(alert.text()).toContain(error);
});
});
describe('Form validation', () => {
describe('Timezone select', () => {
it('has red border when nothing selected', () => {
expect(findTimezoneDropdown().classes()).toContain('invalid-dropdown');
});
it("doesn't have a red border when there is selected opeion", async () => {
findDropdownOptions()
.at(1)
.vm.$emit('click');
await wrapper.vm.$nextTick();
expect(findTimezoneDropdown().classes()).not.toContain('invalid-dropdown');
});
});
});
});
[
{
"identifier": "Etc/GMT+12",
"name": "International Date Line West",
"abbr": "-12",
"formatted_offset": "-12:00"
},
{
"identifier": "Pacific/Pago_Pago",
"name": "American Samoa",
"abbr": "SST",
"formatted_offset": "-11:00"
},
{
"identifier": "Pacific/Midway",
"name": "Midway Island",
"abbr": "SST",
"formatted_offset": "-11:00"
},
{
"identifier": "Pacific/Honolulu",
"name": "Hawaii",
"abbr": "HST",
"formatted_offset": "-10:00"
}
]
...@@ -6,11 +6,13 @@ RSpec.describe IncidentManagement::OncallScheduleHelper do ...@@ -6,11 +6,13 @@ RSpec.describe IncidentManagement::OncallScheduleHelper do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
describe '#oncall_schedule_data' do describe '#oncall_schedule_data' do
subject(:data) { helper.oncall_schedule_data } subject(:data) { helper.oncall_schedule_data(project) }
it 'returns on-call schedule data' do it 'returns on-call schedule data' do
is_expected.to eq( is_expected.to eq(
'empty-oncall-schedules-svg-path' => helper.image_path('illustrations/empty-state/empty-on-call.svg') 'project-path' => project.full_path,
'empty-oncall-schedules-svg-path' => helper.image_path('illustrations/empty-state/empty-on-call.svg'),
'timezones' => helper.timezone_data.to_json
) )
end end
end end
......
...@@ -4881,6 +4881,9 @@ msgstr "" ...@@ -4881,6 +4881,9 @@ msgstr ""
msgid "Can't apply this suggestion." msgid "Can't apply this suggestion."
msgstr "" msgstr ""
msgid "Can't be empty"
msgstr ""
msgid "Can't create snippet: %{err}" msgid "Can't create snippet: %{err}"
msgstr "" msgstr ""
...@@ -9348,6 +9351,9 @@ msgstr "" ...@@ -9348,6 +9351,9 @@ msgstr ""
msgid "Description" msgid "Description"
msgstr "" msgstr ""
msgid "Description (optional)"
msgstr ""
msgid "Description parsed with %{link_start}GitLab Flavored Markdown%{link_end}" msgid "Description parsed with %{link_start}GitLab Flavored Markdown%{link_end}"
msgstr "" msgstr ""
...@@ -19033,12 +19039,24 @@ msgstr "" ...@@ -19033,12 +19039,24 @@ msgstr ""
msgid "OnCallSchedules|Add a schedule" msgid "OnCallSchedules|Add a schedule"
msgstr "" msgstr ""
msgid "OnCallSchedules|Add schedule"
msgstr ""
msgid "OnCallSchedules|Create on-call schedules in GitLab" msgid "OnCallSchedules|Create on-call schedules in GitLab"
msgstr "" msgstr ""
msgid "OnCallSchedules|Failed to add schedule"
msgstr ""
msgid "OnCallSchedules|Route alerts directly to specific members of your team" msgid "OnCallSchedules|Route alerts directly to specific members of your team"
msgstr "" msgstr ""
msgid "OnCallSchedules|Select timezone"
msgstr ""
msgid "OnCallSchedules|Sets the default timezone for the schedule, for all participants"
msgstr ""
msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later." msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later."
msgstr "" msgstr ""
...@@ -28436,6 +28454,9 @@ msgstr "" ...@@ -28436,6 +28454,9 @@ msgstr ""
msgid "Timeout connecting to the Google API. Please try again." msgid "Timeout connecting to the Google API. Please try again."
msgstr "" msgstr ""
msgid "Timezone"
msgstr ""
msgid "Time|hr" msgid "Time|hr"
msgid_plural "Time|hrs" msgid_plural "Time|hrs"
msgstr[0] "" msgstr[0] ""
......
...@@ -4,7 +4,7 @@ require 'spec_helper' ...@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe 'Freeze Periods (JavaScript fixtures)' do RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
include JavaScriptFixturesHelpers include JavaScriptFixturesHelpers
include Ci::PipelineSchedulesHelper include TimeZoneHelper
let_it_be(:admin) { create(:admin) } let_it_be(:admin) { create(:admin) }
let_it_be(:project) { create(:project, :repository, path: 'freeze-periods-project') } let_it_be(:project) { create(:project, :repository, path: 'freeze-periods-project') }
...@@ -40,10 +40,12 @@ RSpec.describe 'Freeze Periods (JavaScript fixtures)' do ...@@ -40,10 +40,12 @@ RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
end end
end end
describe Ci::PipelineSchedulesHelper, '(JavaScript fixtures)' do describe TimeZoneHelper, '(JavaScript fixtures)' do
let(:response) { timezone_data.to_json } let(:response) { timezone_data.to_json }
it 'api/freeze-periods/timezone_data.json' do it 'api/freeze-periods/timezone_data.json' do
# Looks empty but does things
# More info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38525/diffs#note_391048415
end end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::PipelineSchedulesHelper, :aggregate_failures do
describe '#timezone_data' do
subject { helper.timezone_data }
it 'matches schema' do
expect(subject).not_to be_empty
subject.each_with_index do |timzone_hash, i|
expect(timzone_hash.keys).to contain_exactly(:name, :offset, :identifier), "Failed at index #{i}"
end
end
it 'formats for display' do
first_timezone = ActiveSupport::TimeZone.all[0]
expect(subject[0][:name]).to eq(first_timezone.name)
expect(subject[0][:offset]).to eq(first_timezone.now.utc_offset)
expect(subject[0][:identifier]).to eq(first_timezone.tzinfo.identifier)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe TimeZoneHelper, :aggregate_failures do
describe '#timezone_data' do
subject(:timezone_data) { helper.timezone_data }
it 'matches schema' do
expect(timezone_data).not_to be_empty
timezone_data.each_with_index do |timezone_hash, i|
expect(timezone_hash.keys).to contain_exactly(
:identifier,
:name,
:abbr,
:offset,
:formatted_offset
), "Failed at index #{i}"
end
end
it 'formats for display' do
tz = ActiveSupport::TimeZone.all[0]
expect(timezone_data[0]).to eq(
identifier: tz.tzinfo.identifier,
name: tz.name,
abbr: tz.tzinfo.strftime('%Z'),
offset: tz.now.utc_offset,
formatted_offset: tz.now.formatted_offset
)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment