add more e2e tests.
Этот коммит содержится в:
@@ -88,18 +88,18 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.totalCost {
|
.totalCostWarning {
|
||||||
width: 141px;
|
width: 141px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.totalCost > span:first-child {
|
.totalCostWarning > span:first-child {
|
||||||
color: var(--sys-denim-center-channel-text);
|
color: var(--sys-denim-center-channel-text);
|
||||||
font-family: 'Open Sans';
|
font-family: 'Open Sans';
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.totalCost > span:last-child {
|
.totalCostWarning > span:last-child {
|
||||||
color: rgba(var(--sys-denim-center-channel-text-rgb), 0.72);
|
color: rgba(var(--sys-denim-center-channel-text-rgb), 0.72);
|
||||||
font-family: 'Open Sans';
|
font-family: 'Open Sans';
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import {findSelfHostedProductBySku} from 'utils/hosted_customer';
|
|||||||
import ExternalLink from 'components/external_link';
|
import ExternalLink from 'components/external_link';
|
||||||
|
|
||||||
const MONTHS_IN_YEAR = 12;
|
const MONTHS_IN_YEAR = 12;
|
||||||
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
||||||
const MAX_TRANSACTION_VALUE = 1_000_000 - 1;
|
const MAX_TRANSACTION_VALUE = 1_000_000 - 1;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -35,14 +34,14 @@ export default function SelfHostedExpansionCard(props: Props) {
|
|||||||
const endsAt = moment(parseInt(license.ExpiresAt, 10)).format('MMM. D, YYYY');
|
const endsAt = moment(parseInt(license.ExpiresAt, 10)).format('MMM. D, YYYY');
|
||||||
const [additionalSeats, setAdditionalSeats] = useState(props.initialSeats);
|
const [additionalSeats, setAdditionalSeats] = useState(props.initialSeats);
|
||||||
const [overMaxSeats, setOverMaxSeats] = useState(false);
|
const [overMaxSeats, setOverMaxSeats] = useState(false);
|
||||||
const licenseExpiry = parseInt(license.ExpiresAt, 10);
|
const licenseExpiry = new Date(parseInt(license.ExpiresAt, 10));
|
||||||
const invalidAdditionalSeats = additionalSeats === 0 || isNaN(additionalSeats);
|
const invalidAdditionalSeats = additionalSeats === 0 || isNaN(additionalSeats);
|
||||||
const [products] = useGetSelfHostedProducts();
|
const [products] = useGetSelfHostedProducts();
|
||||||
const currentProduct = findSelfHostedProductBySku(products, license.SkuShortName);
|
const currentProduct = findSelfHostedProductBySku(products, license.SkuShortName);
|
||||||
|
|
||||||
const getMonthsUntilExpiry = () => {
|
const getMonthsUntilExpiry = () => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
return Math.ceil((licenseExpiry - now.getTime()) / MILLISECONDS_PER_DAY / 30);
|
return (licenseExpiry.getMonth() - now.getMonth()) + 12 * (licenseExpiry.getFullYear() - now.getFullYear());
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMonthlyPrice = () => {
|
const getMonthlyPrice = () => {
|
||||||
@@ -209,7 +208,7 @@ export default function SelfHostedExpansionCard(props: Props) {
|
|||||||
<br/>
|
<br/>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='self_hosted_expansion_rhs_card_cost_per_user_breakdown'
|
id='self_hosted_expansion_rhs_card_cost_per_user_breakdown'
|
||||||
defaultMessage='{costPerUser} x {monthsUntilExpiry} months'
|
defaultMessage='${costPerUser} x {monthsUntilExpiry} months'
|
||||||
values={{
|
values={{
|
||||||
costPerUser: getMonthlyPrice().toFixed(2),
|
costPerUser: getMonthlyPrice().toFixed(2),
|
||||||
monthsUntilExpiry: getMonthsUntilExpiry(),
|
monthsUntilExpiry: getMonthsUntilExpiry(),
|
||||||
@@ -219,7 +218,7 @@ export default function SelfHostedExpansionCard(props: Props) {
|
|||||||
<div className='costAmount'>
|
<div className='costAmount'>
|
||||||
<span>{'$' + getCostPerUser().toFixed(2)}</span>
|
<span>{'$' + getCostPerUser().toFixed(2)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='totalCost'>
|
<div className='totalCostWarning'>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='self_hosted_expansion_rhs_card_total_title'
|
id='self_hosted_expansion_rhs_card_total_title'
|
||||||
defaultMessage='Total'
|
defaultMessage='Total'
|
||||||
@@ -230,7 +229,7 @@ export default function SelfHostedExpansionCard(props: Props) {
|
|||||||
defaultMessage='The total will be prorated'
|
defaultMessage='The total will be prorated'
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className='costAmount'>
|
<span className='totalCostAmount'>
|
||||||
<span>{'$' + getTotal().toFixed(2)}</span>
|
<span>{'$' + getTotal().toFixed(2)}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {screen, fireEvent} from '@testing-library/react';
|
import {screen, fireEvent, waitFor} from '@testing-library/react';
|
||||||
|
|
||||||
import {GlobalState} from 'types/store';
|
import {GlobalState} from 'types/store';
|
||||||
|
|
||||||
@@ -11,7 +11,7 @@ import {SelfHostedSignupForm, SelfHostedSignupProgress} from '@mattermost/types/
|
|||||||
|
|
||||||
import {renderWithIntlAndStore} from 'tests/react_testing_utils';
|
import {renderWithIntlAndStore} from 'tests/react_testing_utils';
|
||||||
import {TestHelper as TH} from 'utils/test_helper';
|
import {TestHelper as TH} from 'utils/test_helper';
|
||||||
import {SelfHostedProducts, ModalIdentifiers} from 'utils/constants';
|
import {SelfHostedProducts, ModalIdentifiers, RecurringIntervals} from 'utils/constants';
|
||||||
|
|
||||||
import {DeepPartial} from '@mattermost/types/utilities';
|
import {DeepPartial} from '@mattermost/types/utilities';
|
||||||
|
|
||||||
@@ -76,6 +76,7 @@ const mockProfessionalProduct = TH.getProductMock({
|
|||||||
name: 'Professional',
|
name: 'Professional',
|
||||||
sku: SelfHostedProducts.PROFESSIONAL,
|
sku: SelfHostedProducts.PROFESSIONAL,
|
||||||
price_per_seat: 7.5,
|
price_per_seat: 7.5,
|
||||||
|
recurring_interval: RecurringIntervals.MONTH
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('mattermost-redux/client', () => {
|
jest.mock('mattermost-redux/client', () => {
|
||||||
@@ -129,6 +130,11 @@ jest.mock('utils/hosted_customer', () => {
|
|||||||
|
|
||||||
const productName = SelfHostedProducts.PROFESSIONAL;
|
const productName = SelfHostedProducts.PROFESSIONAL;
|
||||||
|
|
||||||
|
// Licensed expiry set as 3 months from the current date (rolls over to new years).
|
||||||
|
const licenseExpiry = new Date();
|
||||||
|
const monthsUntilLicenseExpiry = 3;
|
||||||
|
licenseExpiry.setMonth(licenseExpiry.getMonth() + monthsUntilLicenseExpiry);
|
||||||
|
|
||||||
const initialState: DeepPartial<GlobalState> = {
|
const initialState: DeepPartial<GlobalState> = {
|
||||||
views: {
|
views: {
|
||||||
modals: {
|
modals: {
|
||||||
@@ -143,11 +149,6 @@ const initialState: DeepPartial<GlobalState> = {
|
|||||||
storage: {},
|
storage: {},
|
||||||
},
|
},
|
||||||
entities: {
|
entities: {
|
||||||
admin: {
|
|
||||||
analytics: {
|
|
||||||
TOTAL_USERS: existingUsers,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
teams: {
|
teams: {
|
||||||
currentTeamId: '',
|
currentTeamId: '',
|
||||||
},
|
},
|
||||||
@@ -163,6 +164,7 @@ const initialState: DeepPartial<GlobalState> = {
|
|||||||
license: {
|
license: {
|
||||||
Sku: productName,
|
Sku: productName,
|
||||||
Users: '50',
|
Users: '50',
|
||||||
|
ExpiresAt: licenseExpiry.getTime().toString()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
cloud: {
|
cloud: {
|
||||||
@@ -224,6 +226,7 @@ interface PurchaseForm {
|
|||||||
state: string;
|
state: string;
|
||||||
zip: string;
|
zip: string;
|
||||||
seats: string;
|
seats: string;
|
||||||
|
agree: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultSuccessForm: PurchaseForm = {
|
const defaultSuccessForm: PurchaseForm = {
|
||||||
@@ -236,6 +239,7 @@ const defaultSuccessForm: PurchaseForm = {
|
|||||||
state: 'MN',
|
state: 'MN',
|
||||||
zip: '55423',
|
zip: '55423',
|
||||||
seats: '10',
|
seats: '10',
|
||||||
|
agree: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
function fillForm(form: PurchaseForm) {
|
function fillForm(form: PurchaseForm) {
|
||||||
@@ -248,6 +252,9 @@ function fillForm(form: PurchaseForm) {
|
|||||||
selectDropdownValue('selfHostedExpansionStateSelector', form.state);
|
selectDropdownValue('selfHostedExpansionStateSelector', form.state);
|
||||||
changeByPlaceholder('Zip/Postal Code', form.zip);
|
changeByPlaceholder('Zip/Postal Code', form.zip);
|
||||||
changeByTestId('seatsInput', form.seats);
|
changeByTestId('seatsInput', form.seats);
|
||||||
|
if (form.agree) {
|
||||||
|
fireEvent.click(screen.getByText('I have read and agree', {exact: false}));
|
||||||
|
}
|
||||||
|
|
||||||
expect(document.getElementsByClassName('SelfHostedExpansionRHSCard__AddSeatsWarning')[0] as HTMLElement).toBeEnabled();
|
expect(document.getElementsByClassName('SelfHostedExpansionRHSCard__AddSeatsWarning')[0] as HTMLElement).toBeEnabled();
|
||||||
|
|
||||||
@@ -263,7 +270,7 @@ function fillForm(form: PurchaseForm) {
|
|||||||
return completeButton;
|
return completeButton;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('SelfHostedExpansionModal', () => {
|
describe('SelfHostedExpansionModal Open', () => {
|
||||||
it('renders the form', () => {
|
it('renders the form', () => {
|
||||||
renderWithIntlAndStore(<div id='root-portal'><SelfHostedExpansionModal/></div>, initialState);
|
renderWithIntlAndStore(<div id='root-portal'><SelfHostedExpansionModal/></div>, initialState);
|
||||||
|
|
||||||
@@ -321,7 +328,45 @@ describe('SelfHostedExpansionModal', () => {
|
|||||||
// });
|
// });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('SelfHostedExpansionModal :: canSubmit', () => {
|
describe('SelfHostedExpansionModal RHS Card', () => {
|
||||||
|
it("New seats input should be pre-populated with the difference from the active users and licensed seats", () => {
|
||||||
|
renderWithIntlAndStore(<div id='root-portal'><SelfHostedExpansionModal/></div>, initialState);
|
||||||
|
|
||||||
|
const expectedPrePopulatedSeats = (initialState.entities?.users?.filteredStats?.total_users_count || 1) - parseInt(initialState.entities?.general?.license?.Users || '0', 10);
|
||||||
|
|
||||||
|
const seatsField = screen.getByTestId('seatsInput').querySelector('input');
|
||||||
|
expect(seatsField).toBeInTheDocument();
|
||||||
|
expect(seatsField?.value).toBe(expectedPrePopulatedSeats.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Cost per User should be represented as the current subscription price multiplied by the remaining months", () => {
|
||||||
|
renderWithIntlAndStore(<div id='root-portal'><SelfHostedExpansionModal/></div>, initialState);
|
||||||
|
|
||||||
|
const expectedCostPerUser = monthsUntilLicenseExpiry * mockProfessionalProduct.price_per_seat;
|
||||||
|
|
||||||
|
const costPerUser = document.getElementsByClassName('costPerUser')[0];
|
||||||
|
expect(costPerUser).toBeInTheDocument();
|
||||||
|
expect(costPerUser.innerHTML).toContain('Cost per user<br>$' + mockProfessionalProduct.price_per_seat.toFixed(2) + ' x ' + monthsUntilLicenseExpiry + ' months');
|
||||||
|
|
||||||
|
const costAmount = document.getElementsByClassName('costAmount')[0];
|
||||||
|
expect(costAmount).toBeInTheDocument();
|
||||||
|
expect(costAmount.innerHTML).toContain('$' + expectedCostPerUser)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Total cost User should be represented as the current subscription price multiplied by the remaining months multiplied by the number of users", () => {
|
||||||
|
renderWithIntlAndStore(<div id='root-portal'><SelfHostedExpansionModal/></div>, initialState);
|
||||||
|
const seatsInputValue = 100;
|
||||||
|
changeByTestId('seatsInput', seatsInputValue.toString());
|
||||||
|
|
||||||
|
const expectedTotalCost = monthsUntilLicenseExpiry * mockProfessionalProduct.price_per_seat * seatsInputValue;
|
||||||
|
|
||||||
|
const costAmount = document.getElementsByClassName('totalCostAmount')[0];
|
||||||
|
expect(costAmount).toBeInTheDocument();
|
||||||
|
expect(costAmount.innerHTML).toContain('$' + expectedTotalCost)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('SelfHostedExpansionModal Submit', () => {
|
||||||
function makeHappyPathState(): FormState {
|
function makeHappyPathState(): FormState {
|
||||||
return {
|
return {
|
||||||
address: 'string',
|
address: 'string',
|
||||||
@@ -366,7 +411,6 @@ describe('SelfHostedExpansionModal :: canSubmit', () => {
|
|||||||
expect(canSubmit(state, SelfHostedSignupProgress.PAID)).toBe(true);
|
expect(canSubmit(state, SelfHostedSignupProgress.PAID)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: Needed?
|
|
||||||
it('if created subscription, can submit', () => {
|
it('if created subscription, can submit', () => {
|
||||||
const state = makeInitialState(1);
|
const state = makeInitialState(1);
|
||||||
state.submitting = false;
|
state.submitting = false;
|
||||||
|
|||||||
@@ -409,6 +409,7 @@ export default function SelfHostedExpansionModal() {
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<Address
|
<Address
|
||||||
|
testPrefix='selfHostedExpansion'
|
||||||
type='billing'
|
type='billing'
|
||||||
country={formState.country}
|
country={formState.country}
|
||||||
changeCountry={(option) => {
|
changeCountry={(option) => {
|
||||||
@@ -450,6 +451,7 @@ export default function SelfHostedExpansionModal() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Address
|
<Address
|
||||||
|
testPrefix='selfHostedExpansion'
|
||||||
type='shipping'
|
type='shipping'
|
||||||
country={formState.shippingCountry}
|
country={formState.shippingCountry}
|
||||||
changeCountry={(option) => {
|
changeCountry={(option) => {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user