@@ -5,25 +5,37 @@ import {UserFixture} from 'sentry-fixture/user';
5
5
6
6
import { SubscriptionFixture } from 'getsentry-test/fixtures/subscription' ;
7
7
import { render , screen , userEvent } from 'sentry-test/reactTestingLibrary' ;
8
+ import { resetMockDate , setMockDate } from 'sentry-test/utils' ;
8
9
9
10
import ConfigStore from 'sentry/stores/configStore' ;
10
11
11
12
import PrimaryNavigationQuotaExceeded from 'getsentry/components/navBillingStatus' ;
12
13
import SubscriptionStore from 'getsentry/stores/subscriptionStore' ;
14
+ import { OnDemandBudgetMode } from 'getsentry/types' ;
15
+
16
+ // Jun 06 2022 - with milliseconds
17
+ const MOCK_TODAY = 1654492173000 ;
18
+
19
+ // prompts activity timestamps do not include milliseconds
20
+ const MOCK_PERIOD_START = 1652140800 ; // May 10 2022
21
+ const MOCK_BEFORE_PERIOD_START = 1652054400 ; // May 09 2022
13
22
14
23
describe ( 'PrimaryNavigationQuotaExceeded' , function ( ) {
15
24
const organization = OrganizationFixture ( ) ;
16
25
const subscription = SubscriptionFixture ( {
17
26
organization,
18
27
plan : 'am3_business' ,
28
+ onDemandPeriodStart : '2022-05-10' ,
29
+ onDemandPeriodEnd : '2022-06-09' ,
19
30
} ) ;
20
31
let promptMock : jest . Mock ;
21
32
let requestUpgradeMock : jest . Mock ;
22
33
let customerPutMock : jest . Mock ;
23
34
24
35
beforeEach ( ( ) => {
36
+ setMockDate ( MOCK_TODAY ) ;
37
+ localStorage . clear ( ) ;
25
38
organization . access = [ ] ;
26
- MockApiClient . clearMockResponses ( ) ;
27
39
subscription . categories . errors ! . usageExceeded = true ;
28
40
subscription . categories . replays ! . usageExceeded = true ;
29
41
subscription . categories . spans ! . usageExceeded = true ;
@@ -39,6 +51,7 @@ describe('PrimaryNavigationQuotaExceeded', function () {
39
51
} ,
40
52
} )
41
53
) ;
54
+ MockApiClient . clearMockResponses ( ) ;
42
55
43
56
MockApiClient . addMockResponse ( {
44
57
method : 'GET' ,
@@ -55,25 +68,14 @@ describe('PrimaryNavigationQuotaExceeded', function () {
55
68
url : `/organizations/${ organization . slug } /projects/` ,
56
69
body : [ ProjectFixture ( ) ] ,
57
70
} ) ;
58
- MockApiClient . addMockResponse ( {
59
- method : 'GET' ,
60
- url : `/subscriptions/${ organization . slug } /` ,
61
- body : subscription ,
62
- } ) ;
63
71
MockApiClient . addMockResponse ( {
64
72
method : 'GET' ,
65
73
url : `/organizations/${ organization . slug } /prompts-activity/` ,
66
74
body : { } ,
67
75
} ) ;
68
- MockApiClient . addMockResponse ( {
69
- method : 'PUT' ,
70
- url : `/organizations/${ organization . slug } /prompts-activity/` ,
71
- } ) ;
72
-
73
76
promptMock = MockApiClient . addMockResponse ( {
74
77
method : 'PUT' ,
75
78
url : `/organizations/${ organization . slug } /prompts-activity/` ,
76
- body : { } ,
77
79
} ) ;
78
80
requestUpgradeMock = MockApiClient . addMockResponse ( {
79
81
method : 'POST' ,
@@ -85,8 +87,31 @@ describe('PrimaryNavigationQuotaExceeded', function () {
85
87
url : `/customers/${ organization . slug } /` ,
86
88
body : SubscriptionFixture ( { organization} ) ,
87
89
} ) ;
90
+
91
+ // set localStorage to prevent auto-popup
92
+ localStorage . setItem (
93
+ `billing-status-last-shown-categories-${ organization . id } ` ,
94
+ 'errors-replays-spans' // exceeded categories
95
+ ) ;
96
+ localStorage . setItem (
97
+ `billing-status-last-shown-date-${ organization . id } ` ,
98
+ 'Mon Jun 06 2022' // MOCK_TODAY
99
+ ) ;
88
100
} ) ;
89
101
102
+ afterEach ( ( ) => {
103
+ resetMockDate ( ) ;
104
+ } ) ;
105
+
106
+ function assertLocalStorageStateAfterAutoOpen ( ) {
107
+ expect (
108
+ localStorage . getItem ( `billing-status-last-shown-categories-${ organization . id } ` )
109
+ ) . toBe ( 'errors-replays-spans' ) ;
110
+ expect (
111
+ localStorage . getItem ( `billing-status-last-shown-date-${ organization . id } ` )
112
+ ) . toBe ( 'Mon Jun 06 2022' ) ;
113
+ }
114
+
90
115
it ( 'should render' , async function ( ) {
91
116
render ( < PrimaryNavigationQuotaExceeded organization = { organization } /> ) ;
92
117
@@ -104,7 +129,11 @@ describe('PrimaryNavigationQuotaExceeded', function () {
104
129
expect ( screen . getByRole ( 'checkbox' ) ) . not . toBeChecked ( ) ;
105
130
} ) ;
106
131
107
- it ( 'should render PAYG categories when there is PAYG' , async function ( ) {
132
+ it ( 'should render PAYG categories when there is shared PAYG' , async function ( ) {
133
+ localStorage . setItem (
134
+ `billing-status-last-shown-categories-${ organization . id } ` ,
135
+ 'errors-replays-spans-monitorSeats-profileDuration' // exceeded categories
136
+ ) ;
108
137
subscription . onDemandMaxSpend = 100 ;
109
138
SubscriptionStore . set ( organization . slug , subscription ) ;
110
139
render ( < PrimaryNavigationQuotaExceeded organization = { organization } /> ) ;
@@ -123,6 +152,44 @@ describe('PrimaryNavigationQuotaExceeded', function () {
123
152
subscription . onDemandMaxSpend = 0 ;
124
153
} ) ;
125
154
155
+ it ( 'should render PAYG categories with per category PAYG' , async function ( ) {
156
+ localStorage . setItem (
157
+ `billing-status-last-shown-categories-${ organization . id } ` ,
158
+ 'errors-replays-spans-monitorSeats' // exceeded categories
159
+ ) ;
160
+ subscription . onDemandBudgets = {
161
+ budgetMode : OnDemandBudgetMode . PER_CATEGORY ,
162
+ budgets : {
163
+ monitorSeats : 100 ,
164
+ } ,
165
+ usedSpends : { } ,
166
+ enabled : true ,
167
+ attachmentsBudget : 0 ,
168
+ errorsBudget : 0 ,
169
+ replaysBudget : 0 ,
170
+ transactionsBudget : 0 ,
171
+ attachmentSpendUsed : 0 ,
172
+ errorSpendUsed : 0 ,
173
+ transactionSpendUsed : 0 ,
174
+ } ;
175
+ SubscriptionStore . set ( organization . slug , subscription ) ;
176
+ render ( < PrimaryNavigationQuotaExceeded organization = { organization } /> ) ;
177
+
178
+ // open the alert
179
+ await userEvent . click ( await screen . findByRole ( 'button' , { name : 'Billing Status' } ) ) ;
180
+ expect ( await screen . findByText ( 'Quota Exceeded' ) ) . toBeInTheDocument ( ) ;
181
+ expect (
182
+ screen . getByText (
183
+ / Y o u ’ v e r u n o u t o f e r r o r s , r e p l a y s , s p a n s , a n d c r o n m o n i t o r s f o r t h i s b i l l i n g c y c l e ./
184
+ )
185
+ ) . toBeInTheDocument ( ) ;
186
+ expect ( screen . getByRole ( 'checkbox' ) ) . not . toBeChecked ( ) ;
187
+
188
+ // reset
189
+ subscription . onDemandMaxSpend = 0 ;
190
+ subscription . onDemandBudgets = undefined ;
191
+ } ) ;
192
+
126
193
it ( 'should not render for managed orgs' , function ( ) {
127
194
subscription . canSelfServe = false ;
128
195
SubscriptionStore . set ( organization . slug , subscription ) ;
@@ -145,29 +212,7 @@ describe('PrimaryNavigationQuotaExceeded', function () {
145
212
146
213
// stop the alert from animating
147
214
await userEvent . click ( screen . getByRole ( 'checkbox' ) ) ;
148
- expect ( promptMock ) . toHaveBeenCalled ( ) ;
149
-
150
- // overlay is still visible, need to click out to close
151
- expect ( screen . getByText ( 'Quota Exceeded' ) ) . toBeInTheDocument ( ) ;
152
- await userEvent . click ( document . body ) ;
153
- expect ( screen . queryByText ( 'Quota Exceeded' ) ) . not . toBeInTheDocument ( ) ;
154
-
155
- // open again
156
- await userEvent . click ( await screen . findByRole ( 'button' , { name : 'Billing Status' } ) ) ;
157
- expect ( await screen . findByText ( 'Quota Exceeded' ) ) . toBeInTheDocument ( ) ;
158
- expect ( screen . getByRole ( 'checkbox' ) ) . toBeChecked ( ) ;
159
-
160
- // uncheck the checkbox
161
- await userEvent . click ( screen . getByRole ( 'checkbox' ) ) ;
162
- expect ( promptMock ) . toHaveBeenCalled ( ) ;
163
-
164
- // close and open again
165
- expect ( screen . getByText ( 'Quota Exceeded' ) ) . toBeInTheDocument ( ) ;
166
- await userEvent . click ( document . body ) ;
167
- expect ( screen . queryByText ( 'Quota Exceeded' ) ) . not . toBeInTheDocument ( ) ;
168
- await userEvent . click ( await screen . findByRole ( 'button' , { name : 'Billing Status' } ) ) ;
169
- expect ( await screen . findByText ( 'Quota Exceeded' ) ) . toBeInTheDocument ( ) ;
170
- expect ( screen . getByRole ( 'checkbox' ) ) . not . toBeChecked ( ) ;
215
+ expect ( promptMock ) . toHaveBeenCalledTimes ( 3 ) ; // one for each category
171
216
} ) ;
172
217
173
218
it ( 'should update prompts when non-billing user takes action' , async function ( ) {
@@ -180,9 +225,8 @@ describe('PrimaryNavigationQuotaExceeded', function () {
180
225
181
226
// click the button
182
227
await userEvent . click ( screen . getByText ( 'Request Additional Quota' ) ) ;
183
- expect ( promptMock ) . toHaveBeenCalled ( ) ;
228
+ expect ( promptMock ) . toHaveBeenCalledTimes ( 3 ) ;
184
229
expect ( requestUpgradeMock ) . toHaveBeenCalled ( ) ;
185
- expect ( screen . getByRole ( 'checkbox' ) ) . toBeChecked ( ) ;
186
230
} ) ;
187
231
188
232
it ( 'should update prompts when billing user on free plan takes action' , async function ( ) {
@@ -193,6 +237,16 @@ describe('PrimaryNavigationQuotaExceeded', function () {
193
237
} ) ;
194
238
freeSub . categories . replays ! . usageExceeded = true ;
195
239
SubscriptionStore . set ( organization . slug , freeSub ) ;
240
+ localStorage . setItem (
241
+ `billing-status-last-shown-categories-${ organization . id } ` ,
242
+ 'replays'
243
+ ) ;
244
+ MockApiClient . addMockResponse ( {
245
+ method : 'GET' ,
246
+ url : `/subscriptions/${ organization . slug } /` ,
247
+ body : freeSub ,
248
+ } ) ;
249
+
196
250
render ( < PrimaryNavigationQuotaExceeded organization = { organization } /> ) ;
197
251
198
252
// open the alert
@@ -202,7 +256,113 @@ describe('PrimaryNavigationQuotaExceeded', function () {
202
256
203
257
// click the button
204
258
await userEvent . click ( screen . getByText ( 'Start Trial' ) ) ;
205
- expect ( promptMock ) . toHaveBeenCalled ( ) ;
259
+ expect ( promptMock ) . toHaveBeenCalledTimes ( 1 ) ;
206
260
expect ( customerPutMock ) . toHaveBeenCalled ( ) ;
207
261
} ) ;
262
+
263
+ it ( 'should auto open based on localStorage' , async function ( ) {
264
+ render ( < PrimaryNavigationQuotaExceeded organization = { organization } /> ) ;
265
+ expect (
266
+ await screen . findByRole ( 'button' , { name : 'Billing Status' } )
267
+ ) . toBeInTheDocument ( ) ;
268
+ expect ( screen . queryByText ( 'Quota Exceeded' ) ) . not . toBeInTheDocument ( ) ;
269
+
270
+ localStorage . clear ( ) ;
271
+ render ( < PrimaryNavigationQuotaExceeded organization = { organization } /> ) ;
272
+ expect (
273
+ await screen . findByRole ( 'button' , { name : 'Billing Status' } )
274
+ ) . toBeInTheDocument ( ) ;
275
+ expect ( await screen . findByText ( 'Quota Exceeded' ) ) . toBeInTheDocument ( ) ;
276
+ assertLocalStorageStateAfterAutoOpen ( ) ;
277
+ } ) ;
278
+
279
+ it ( 'should not auto open if explicitly dismissed' , async function ( ) {
280
+ MockApiClient . addMockResponse ( {
281
+ method : 'GET' ,
282
+ url : `/organizations/${ organization . slug } /prompts-activity/` ,
283
+ body : {
284
+ features : {
285
+ errors_overage_alert : {
286
+ snoozed_ts : MOCK_PERIOD_START ,
287
+ } ,
288
+ replays_overage_alert : {
289
+ snoozed_ts : MOCK_PERIOD_START ,
290
+ } ,
291
+ spans_overage_alert : {
292
+ snoozed_ts : MOCK_PERIOD_START ,
293
+ } ,
294
+ } ,
295
+ } , // dismissed at beginning of billing cycle
296
+ } ) ;
297
+ render ( < PrimaryNavigationQuotaExceeded organization = { organization } /> ) ;
298
+ expect (
299
+ await screen . findByRole ( 'button' , { name : 'Billing Status' } )
300
+ ) . toBeInTheDocument ( ) ;
301
+ expect ( screen . queryByText ( 'Quota Exceeded' ) ) . not . toBeInTheDocument ( ) ;
302
+
303
+ // even when localStorage is cleared, the alert should not show
304
+ localStorage . clear ( ) ;
305
+ render ( < PrimaryNavigationQuotaExceeded organization = { organization } /> ) ;
306
+ expect (
307
+ await screen . findByRole ( 'button' , { name : 'Billing Status' } )
308
+ ) . toBeInTheDocument ( ) ;
309
+ expect ( screen . queryByText ( 'Quota Exceeded' ) ) . not . toBeInTheDocument ( ) ;
310
+ expect ( localStorage ) . toHaveLength ( 0 ) ;
311
+ } ) ;
312
+
313
+ it ( 'should auto open if explicitly dismissed before current billing cycle' , async function ( ) {
314
+ MockApiClient . addMockResponse ( {
315
+ method : 'GET' ,
316
+ url : `/organizations/${ organization . slug } /prompts-activity/` ,
317
+ body : {
318
+ features : {
319
+ errors_overage_alert : {
320
+ snoozed_ts : MOCK_BEFORE_PERIOD_START ,
321
+ } ,
322
+ replays_overage_alert : {
323
+ snoozed_ts : MOCK_BEFORE_PERIOD_START ,
324
+ } ,
325
+ spans_overage_alert : {
326
+ snoozed_ts : MOCK_BEFORE_PERIOD_START ,
327
+ } ,
328
+ } ,
329
+ } , // dismissed on last day before current billing cycle
330
+ } ) ;
331
+ localStorage . clear ( ) ;
332
+ render ( < PrimaryNavigationQuotaExceeded organization = { organization } /> ) ;
333
+ expect ( await screen . findByText ( 'Quota Exceeded' ) ) . toBeInTheDocument ( ) ;
334
+ } ) ;
335
+
336
+ it ( 'should auto open the alert when categories have changed' , async function ( ) {
337
+ render ( < PrimaryNavigationQuotaExceeded organization = { organization } /> ) ;
338
+ expect ( screen . queryByText ( 'Quota Exceeded' ) ) . not . toBeInTheDocument ( ) ;
339
+
340
+ localStorage . setItem (
341
+ `billing-status-last-shown-categories-${ organization . id } ` ,
342
+ 'errors-replays'
343
+ ) ; // spans not included, so alert should show even though last opened "today"
344
+ render ( < PrimaryNavigationQuotaExceeded organization = { organization } /> ) ;
345
+ expect ( await screen . findByText ( 'Quota Exceeded' ) ) . toBeInTheDocument ( ) ;
346
+ assertLocalStorageStateAfterAutoOpen ( ) ;
347
+ } ) ;
348
+
349
+ it ( 'should auto open the alert when more than a day has passed' , async function ( ) {
350
+ localStorage . setItem (
351
+ `billing-status-last-shown-date-${ organization . id } ` ,
352
+ 'Sun Jun 05 2022'
353
+ ) ; // more than a day, so alert should show even though categories haven't changed
354
+ render ( < PrimaryNavigationQuotaExceeded organization = { organization } /> ) ;
355
+ expect ( await screen . findByText ( 'Quota Exceeded' ) ) . toBeInTheDocument ( ) ;
356
+ assertLocalStorageStateAfterAutoOpen ( ) ;
357
+ } ) ;
358
+
359
+ it ( 'should auto open the alert when the last shown date is before the current usage cycle started' , async function ( ) {
360
+ localStorage . setItem (
361
+ `billing-status-last-shown-date-${ organization . id } ` ,
362
+ 'Sun May 29 2022'
363
+ ) ; // last seen before current usage cycle started, so alert should show even though categories haven't changed
364
+ render ( < PrimaryNavigationQuotaExceeded organization = { organization } /> ) ;
365
+ expect ( await screen . findByText ( 'Quota Exceeded' ) ) . toBeInTheDocument ( ) ;
366
+ assertLocalStorageStateAfterAutoOpen ( ) ;
367
+ } ) ;
208
368
} ) ;
0 commit comments