Commit 9f6e4149 authored by Doug Stull's avatar Doug Stull

Reuse the country and state selector for hand raise leads

- DRY up code and logic
parent e5435114
......@@ -29,7 +29,6 @@ RSpec/AnyInstanceOf:
- ee/spec/features/security/project/internal_access_spec.rb
- ee/spec/features/security/project/private_access_spec.rb
- ee/spec/features/security/project/public_access_spec.rb
- ee/spec/features/trials/capture_lead_spec.rb
- ee/spec/features/trials/select_namespace_spec.rb
- ee/spec/features/users/login_spec.rb
- ee/spec/graphql/mutations/dast_on_demand_scans/create_spec.rb
......
......@@ -14,9 +14,8 @@ import { sprintf } from '~/locale';
import Tracking from '~/tracking';
import countriesQuery from 'ee/subscriptions/graphql/queries/countries.query.graphql';
import statesQuery from 'ee/subscriptions/graphql/queries/states.query.graphql';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import CountryOrRegionSelector from 'ee/trials/components/country_or_region_selector.vue';
import {
COUNTRIES_WITH_STATES_ALLOWED,
LEADS_COMPANY_NAME_LABEL,
LEADS_COMPANY_SIZE_LABEL,
LEADS_COUNTRY_LABEL,
......@@ -51,10 +50,10 @@ export default {
GlFormSelect,
GlFormTextarea,
GlModal,
CountryOrRegionSelector,
},
directives: {
GlModal: GlModalDirective,
autofocusonshow,
},
mixins: [Tracking.mixin()],
inject: ['user', 'small'],
......@@ -66,11 +65,12 @@ export default {
companyName: this.user.companyName,
companySize: null,
phoneNumber: '',
country: null,
state: null,
country: '',
state: '',
countries: [],
states: [],
comment: '',
stateRequired: false,
};
},
apollo: {
......@@ -95,9 +95,6 @@ export default {
userName: this.user.userName,
});
},
mustEnterState() {
return COUNTRIES_WITH_STATES_ALLOWED.includes(this.country);
},
canSubmit() {
return (
this.firstName &&
......@@ -106,7 +103,7 @@ export default {
this.companySize &&
this.phoneNumber &&
this.country &&
(this.mustEnterState ? this.state : true)
(this.stateRequired ? this.state : true)
);
},
actionPrimary() {
......@@ -125,9 +122,6 @@ export default {
label: 'hand_raise_lead_form',
};
},
showState() {
return !this.$apollo.loading.states && this.states && this.country && this.mustEnterState;
},
companySizeOptionsWithDefault() {
return [
{
......@@ -137,24 +131,6 @@ export default {
...companySizes,
];
},
countryOptionsWithDefault() {
return [
{
name: this.$options.i18n.countrySelectPrompt,
id: null,
},
...this.countries,
];
},
stateOptionsWithDefault() {
return [
{
name: this.$options.i18n.stateSelectPrompt,
id: null,
},
...this.states,
];
},
formParams() {
return {
namespaceId: Number(this.user.namespaceId),
......@@ -164,7 +140,7 @@ export default {
companySize: this.companySize,
phoneNumber: this.phoneNumber,
country: this.country,
state: this.mustEnterState ? this.state : null,
state: this.stateRequired ? this.state : null,
comment: this.comment,
glmContent: this.user.glmContent,
};
......@@ -177,9 +153,10 @@ export default {
this.companyName = '';
this.companySize = null;
this.phoneNumber = '';
this.country = null;
this.state = null;
this.country = '';
this.state = '';
this.comment = '';
this.stateRequired = false;
},
async submit() {
this.isLoading = true;
......@@ -205,6 +182,11 @@ export default {
this.isLoading = false;
});
},
onChange({ country, state, stateRequired }) {
this.country = country;
this.state = state;
this.stateRequired = stateRequired;
},
},
i18n: {
firstNameLabel: LEADS_FIRST_NAME_LABEL,
......@@ -247,6 +229,7 @@ export default {
<gl-modal
ref="modal"
modal-id="hand-raise-lead"
data-testid="hand-raise-lead-modal"
size="sm"
:title="$options.i18n.modalTitle"
:action-primary="actionPrimary"
......@@ -309,7 +292,7 @@ export default {
>
<gl-form-select
v-model="companySize"
v-autofocusonshow
name="company-size"
:options="companySizeOptionsWithDefault"
value-field="id"
text-field="name"
......@@ -331,37 +314,7 @@ export default {
data-testid="phone-number"
/>
</gl-form-group>
<gl-form-group
v-if="!$apollo.loading.countries"
:label="$options.i18n.countryLabel"
label-size="sm"
label-for="country"
>
<gl-form-select
v-model="country"
v-autofocusonshow
:options="countryOptionsWithDefault"
value-field="id"
text-field="name"
data-testid="country"
/>
</gl-form-group>
<gl-form-group
v-if="showState"
:label="$options.i18n.stateLabel"
label-size="sm"
label-for="state"
>
<gl-form-select
v-model="state"
v-autofocusonshow
:options="stateOptionsWithDefault"
value-field="id"
text-field="name"
data-testid="state"
/>
</gl-form-group>
<country-or-region-selector :country="country" :state="state" @change="onChange" />
<gl-form-group :label="$options.i18n.commentLabel" label-size="sm" label-for="comment">
<gl-form-textarea v-model="comment" />
</gl-form-group>
......
......@@ -19,9 +19,23 @@ export default {
directives: {
autofocusonshow,
},
inject: ['user'],
props: {
country: {
type: String,
required: true,
},
state: {
type: String,
required: true,
},
required: {
type: Boolean,
default: false,
required: false,
},
},
data() {
return { ...this.user, countries: [], states: [] };
return { selectedCountry: this.country, selectedState: this.state, countries: [], states: [] };
},
i18n: {
countryLabel: LEADS_COUNTRY_LABEL,
......@@ -34,27 +48,45 @@ export default {
return [
{
name: this.$options.i18n.countrySelectPrompt,
id: null,
id: '',
},
...this.countries,
];
},
mustEnterState() {
return COUNTRIES_WITH_STATES_ALLOWED.includes(this.country);
stateRequired() {
return COUNTRIES_WITH_STATES_ALLOWED.includes(this.selectedCountry);
},
showState() {
return !this.$apollo.loading.states && this.states && this.country && this.mustEnterState;
return (
!this.$apollo.loading.states && this.states && this.selectedCountry && this.stateRequired
);
},
stateOptionsWithDefault() {
return [
{
name: this.$options.i18n.stateSelectPrompt,
id: null,
id: '',
},
...this.states,
];
},
},
methods: {
selected() {
this.setSelectedState();
this.$emit('change', {
country: this.selectedCountry,
state: this.selectedState,
stateRequired: this.stateRequired,
});
},
setSelectedState() {
if (!this.showState) {
this.selectedState = '';
}
},
},
apollo: {
countries: {
query: countriesQuery,
......@@ -62,11 +94,11 @@ export default {
states: {
query: statesQuery,
skip() {
return !this.country;
return !this.selectedCountry;
},
variables() {
return {
countryId: this.country,
countryId: this.selectedCountry,
};
},
},
......@@ -84,14 +116,15 @@ export default {
>
<gl-form-select
id="country"
v-model="country"
v-model="selectedCountry"
name="country"
:options="countryOptionsWithDefault"
value-field="id"
text-field="name"
data-qa-selector="country"
data-testid="country"
required
:required="required"
@change="selected"
/>
</gl-form-group>
<gl-form-group
......@@ -102,14 +135,15 @@ export default {
>
<gl-form-select
id="state"
v-model="selectedState"
v-autofocusonshow
:value="state"
name="state"
:options="stateOptionsWithDefault"
value-field="id"
text-field="name"
data-testid="state"
required
:required="required"
@change="selected"
/>
</gl-form-group>
</div>
......
......@@ -123,7 +123,7 @@ export default {
required
/>
</gl-form-group>
<country-or-region-selector />
<country-or-region-selector :country="country" :state="state" required />
<gl-form-group
:label="$options.i18n.phoneNumberLabel"
label-size="sm"
......
......@@ -25,8 +25,8 @@ export const initTrialCreateLeadForm = () => {
lastName,
companyName,
companySize: companySize || null,
country: country || null,
state: state || null,
country: country || '',
state: state || '',
phoneNumber,
},
submitPath,
......
......@@ -70,13 +70,56 @@ RSpec.describe 'Billing plan pages', :feature, :js do
it 'displays the in-app hand raise lead', :aggregate_failures do
if namespace.group_namespace?
form_data = {
first_name: user.first_name,
last_name: user.last_name,
phone_number: '+1 23 456-78-90',
company_size: '1 - 99',
company_name: user.organization,
country: { id: 'US', name: 'United States of America' },
state: { id: 'CA', name: 'California' }
}
hand_raise_lead_params = {
"first_name" => form_data[:first_name],
"last_name" => form_data[:last_name],
"company_name" => form_data[:company_name],
"company_size" => form_data[:company_size].delete(' '),
"phone_number" => form_data[:phone_number],
"country" => form_data.dig(:country, :id),
"state" => form_data.dig(:state, :id),
"namespace_id" => namespace.id,
"comment" => '',
"glm_content" => 'billing-group',
"work_email" => user.email,
"uid" => user.id,
"setup_for_company" => user.setup_for_company,
"provider" => "gitlab",
"glm_source" => 'gitlab.com'
}
lead_params = ActionController::Parameters.new(hand_raise_lead_params).permit!
expect_next_instance_of(GitlabSubscriptions::CreateHandRaiseLeadService) do |service|
expect(service).to receive(:execute).with(lead_params).and_return(double('lead', success?: true ))
end
page.within('[data-testid="plan-card-premium"]') do
click_button 'Contact sales'
end
expect(page).to have_content('Contact our Sales team')
expect(page).to have_field('First Name', with: 'James')
expect(page).to have_field('Last Name', with: 'Bond')
expect(page).to have_field('Company Name', with: 'ACME')
expect(page).to have_field('First Name', with: form_data[:first_name])
expect(page).to have_field('Last Name', with: form_data[:last_name])
expect(page).to have_field('Company Name', with: form_data[:company_name])
page.within('[data-testid="hand-raise-lead-modal"]') do
select form_data[:company_size], from: 'company-size'
fill_in 'phone-number', with: form_data[:phone_number]
select form_data.dig(:country, :name), from: 'country'
select form_data.dig(:state, :name), from: 'state'
click_button 'Submit information'
end
else
expect(page).to have_selector(".js-hand-raise-lead-button[data-namespace-id='#{namespace.id}'][data-user-name='#{user.username}']", visible: false)
end
......
......@@ -47,8 +47,8 @@ RSpec.describe 'Trial Capture Lead', :js do
trial_user: ActionController::Parameters.new(trial_user_params).permit!
}
expect_any_instance_of(GitlabSubscriptions::CreateLeadService).to receive(:execute).with(lead_params) do
{ success: true }
expect_next_instance_of(GitlabSubscriptions::CreateLeadService) do |service|
expect(service).to receive(:execute).with(lead_params).and_return({ success: true })
end
end
......
......@@ -3,7 +3,6 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { sprintf } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking } from 'helpers/tracking_helper';
import HandRaiseLeadButton from 'ee/hand_raise_leads/hand_raise_lead/components/hand_raise_lead_button.vue';
import {
......@@ -14,30 +13,16 @@ import {
PQL_MODAL_FOOTER_TEXT,
} from 'ee/hand_raise_leads/hand_raise_lead/constants';
import * as SubscriptionsApi from 'ee/api/subscriptions_api';
import { formData, states, countries } from './mock_data';
import { FORM_DATA } from './mock_data';
Vue.use(VueApollo);
describe('HandRaiseLeadButton', () => {
let wrapper;
let fakeApollo;
let trackingSpy;
const createComponent = (small = false) => {
const mockResolvers = {
Query: {
countries() {
return [{ id: 'US', name: 'United States' }];
},
states() {
return [{ countryId: 'US', id: 'CA', name: 'California' }];
},
},
};
fakeApollo = createMockApollo([], mockResolvers);
return shallowMountExtended(HandRaiseLeadButton, {
apolloProvider: fakeApollo,
provide: {
small,
user: {
......@@ -59,7 +44,6 @@ describe('HandRaiseLeadButton', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
fakeApollo = null;
});
describe('rendering', () => {
......@@ -87,7 +71,6 @@ describe('HandRaiseLeadButton', () => {
{ id: 'company-name', value: 'ACME' },
{ id: 'phone-number', value: '' },
{ id: 'company-size', value: undefined },
{ id: 'country', value: undefined },
];
formInputValues.forEach(({ id, value }) => {
......@@ -104,7 +87,6 @@ describe('HandRaiseLeadButton', () => {
'company-name',
'company-size',
'phone-number',
'country',
];
visibleFields.forEach((f) => expect(wrapper.findByTestId(f).exists()).toBe(true));
......@@ -155,7 +137,7 @@ describe('HandRaiseLeadButton', () => {
it('becomes enabled when required info is there', async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ countries, states, ...formData });
wrapper.setData({ ...FORM_DATA });
await nextTick();
......@@ -166,27 +148,6 @@ describe('HandRaiseLeadButton', () => {
});
});
describe('country & state handling', () => {
beforeEach(() => {
wrapper = createComponent();
});
it.each`
state | display
${'US'} | ${true}
${'CA'} | ${true}
${'NL'} | ${false}
`('displayed $display', async ({ state, display }) => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ countries, states, country: state });
await nextTick();
expect(wrapper.findByTestId('state').exists()).toBe(display);
});
});
describe('form', () => {
beforeEach(async () => {
wrapper = createComponent();
......@@ -194,7 +155,7 @@ describe('HandRaiseLeadButton', () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ countries, states, country: 'US', ...formData, comment: 'comment' });
wrapper.setData({ ...FORM_DATA, stateRequired: true, comment: 'comment' });
});
describe('successful submission', () => {
......@@ -209,7 +170,7 @@ describe('HandRaiseLeadButton', () => {
namespaceId: 1,
comment: 'comment',
glmContent: 'some-content',
...formData,
...FORM_DATA,
});
});
......@@ -218,11 +179,10 @@ describe('HandRaiseLeadButton', () => {
expect(wrapper.findByTestId(f).attributes('value')).toBe(''),
);
['company-size', 'country'].forEach((f) =>
expect(wrapper.findByTestId(f).attributes('value')).toBe(undefined),
);
expect(wrapper.findByTestId('state').exists()).toBe(false);
expect(wrapper.findByTestId('company-size').attributes('value')).toBe(undefined);
expect(wrapper.vm.country).toBe('');
expect(wrapper.vm.state).toBe('');
expect(wrapper.vm.stateRequired).toBe(false);
});
it('tracks successful submission', async () => {
......
export const countries = [
{ id: 'US', name: 'United States' },
export const COUNTRY_WITH_STATES = 'US';
export const STATE = 'CA';
export const COUNTRIES = [
{ id: COUNTRY_WITH_STATES, name: 'United States' },
{ id: 'CA', name: 'Canada' },
{ id: 'NL', name: 'Netherlands' },
];
export const states = [
{ countryId: 'US', id: 'CA', name: 'California' },
export const STATES = [
{ countryId: COUNTRY_WITH_STATES, id: STATE, name: 'California' },
{ countryId: 'CA', id: 'BC', name: 'British Columbia' },
];
export const formData = {
export const FORM_DATA = {
firstName: 'Joe',
lastName: 'Doe',
companyName: 'ACME',
companySize: '1-99',
phoneNumber: '192919',
country: 'US',
state: 'CA',
country: COUNTRY_WITH_STATES,
state: STATE,
};
......@@ -3,30 +3,36 @@ import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import CountryOrRegionSelector from 'ee/trials/components/country_or_region_selector.vue';
import { countries, states } from '../../hand_raise_leads/components/mock_data';
import { formData } from './mock_data';
import {
COUNTRIES,
STATES,
COUNTRY_WITH_STATES,
STATE,
} from '../../hand_raise_leads/components/mock_data';
Vue.use(VueApollo);
describe('CountryOrRegionSelector', () => {
let wrapper;
const createComponent = ({ mountFunction = shallowMountExtended } = {}) => {
const createComponent = (props = {}) => {
const mockResolvers = {
Query: {
countries() {
return [{ id: 'US', name: 'United States' }];
return COUNTRIES;
},
states() {
return [{ countryId: 'US', id: 'CA', name: 'California' }];
return STATES;
},
},
};
return mountFunction(CountryOrRegionSelector, {
return shallowMountExtended(CountryOrRegionSelector, {
apolloProvider: createMockApollo([], mockResolvers),
provide: {
user: formData,
propsData: {
country: COUNTRY_WITH_STATES,
state: STATE,
...props,
},
});
};
......@@ -49,12 +55,6 @@ describe('CountryOrRegionSelector', () => {
`('has the default injected value for $testid', ({ testid, value }) => {
expect(findFormInput(testid).attributes('value')).toBe(value);
});
it('has the correct form input in the form content', () => {
const visibleFields = ['country', 'state'];
visibleFields.forEach((f) => expect(wrapper.findByTestId(f).exists()).toBe(true));
});
});
describe.each`
......@@ -65,18 +65,66 @@ describe('CountryOrRegionSelector', () => {
`('Country & State handling', ({ country, display }) => {
describe(`when provided country is set to ${country}`, () => {
beforeEach(() => {
wrapper = createComponent();
wrapper = createComponent({ country });
});
it(`should${display ? '' : ' not'} render the state`, async () => {
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ countries, states, country });
await nextTick();
expect(findFormInput('state').exists()).toBe(display);
});
});
});
describe('selection change', () => {
it('emits the change event properly when country is changed', async () => {
wrapper = createComponent();
await findFormInput('country').vm.$emit('change', true);
expect(wrapper.emitted('change')[0]).toStrictEqual([
{ country: 'US', state: 'CA', stateRequired: true },
]);
});
it('emits the change event properly when country is changed with no state required', async () => {
wrapper = createComponent({ country: 'NL' });
await findFormInput('country').vm.$emit('change', true);
expect(wrapper.emitted('change')[0]).toStrictEqual([
{ country: 'NL', state: '', stateRequired: false },
]);
});
it('emits the change event properly when country is changed with state required', async () => {
wrapper = createComponent({ country: 'US', state: '' });
await findFormInput('country').vm.$emit('change', true);
expect(wrapper.emitted('change')[0]).toStrictEqual([
{ country: 'US', state: '', stateRequired: true },
]);
});
it('emits the change event properly when state is not required but has value', async () => {
wrapper = createComponent({ country: 'NL', state: 'CA' });
await findFormInput('country').vm.$emit('change', true);
expect(wrapper.emitted('change')[0]).toStrictEqual([
{ country: 'NL', state: '', stateRequired: false },
]);
});
it('emits the change event properly when state is changed', async () => {
wrapper = createComponent();
await findFormInput('state').vm.$emit('change', true);
expect(wrapper.emitted('change')[0]).toStrictEqual([
{ country: 'US', state: 'CA', stateRequired: true },
]);
});
});
});
export const submitPath = '_submit_path_';
export const formData = {
export const SUBMIT_PATH = '_submit_path_';
export const FORM_DATA = {
firstName: 'Joe',
lastName: 'Doe',
companyName: 'ACME',
......
......@@ -5,7 +5,7 @@ import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_help
import TrialCreateLeadForm from 'ee/trials/components/trial_create_lead_form.vue';
import { TRIAL_FORM_SUBMIT_TEXT } from 'ee/trials/constants';
import { trackSaasTrialSubmit } from '~/google_tag_manager';
import { formData, submitPath } from './mock_data';
import { FORM_DATA, SUBMIT_PATH } from './mock_data';
jest.mock('~/google_tag_manager', () => ({
trackSaasTrialSubmit: jest.fn(),
......@@ -21,8 +21,8 @@ describe('TrialCreateLeadForm', () => {
return mountFunction(TrialCreateLeadForm, {
localVue,
provide: {
submitPath,
user: formData,
submitPath: SUBMIT_PATH,
user: FORM_DATA,
},
});
};
......
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