3
3
*/
4
4
5
5
import { LocalStorageEngine } from './local-storage-engine' ;
6
- import { StringValuedAsyncStore , StorageFullUnableToWrite , LocalStorageUnknownFailure } from './string-valued.store' ;
6
+ import {
7
+ StringValuedAsyncStore ,
8
+ StorageFullUnableToWrite ,
9
+ LocalStorageUnknownFailure ,
10
+ } from './string-valued.store' ;
7
11
8
12
describe ( 'LocalStorageStore' , ( ) => {
9
13
// Note: window.localStorage is mocked for the node environment via the jsdom jest environment
@@ -101,55 +105,63 @@ describe('LocalStorageStore', () => {
101
105
} ) ;
102
106
103
107
describe ( 'StorageFullUnableToWrite exception handling' , ( ) => {
104
- let mockLocalStorage : Storage ;
108
+ // We need to mock localStorage with a controllable length property for testing
109
+ // The Storage interface has a read-only 'length' property, so we extend it with
110
+ // a private '_length' property that we can modify in tests
111
+ let mockLocalStorage : Storage & { _length : number } ;
105
112
let localStorageEngineWithMock : LocalStorageEngine ;
106
113
107
114
beforeEach ( ( ) => {
108
115
mockLocalStorage = {
116
+ get length ( ) {
117
+ return this . _length || 0 ;
118
+ } ,
119
+ _length : 0 ,
109
120
getItem : jest . fn ( ) ,
110
121
setItem : jest . fn ( ) ,
111
122
removeItem : jest . fn ( ) ,
112
123
clear : jest . fn ( ) ,
113
- length : 0 ,
114
124
key : jest . fn ( ) ,
115
- } ;
125
+ } as Storage & { _length : number } ;
116
126
localStorageEngineWithMock = new LocalStorageEngine ( mockLocalStorage , 'test' ) ;
117
127
} ) ;
118
128
119
129
it ( 'should throw StorageFullUnableToWrite when setContentsJsonString fails after clear and retry' , async ( ) => {
120
130
const quotaError = new DOMException ( 'QuotaExceededError' , 'QuotaExceededError' ) ;
121
131
Object . defineProperty ( quotaError , 'code' , { value : DOMException . QUOTA_EXCEEDED_ERR } ) ;
122
-
132
+
123
133
( mockLocalStorage . setItem as jest . Mock ) . mockImplementation ( ( ) => {
124
134
throw quotaError ;
125
135
} ) ;
126
136
( mockLocalStorage . key as jest . Mock ) . mockReturnValue ( null ) ;
127
- ( mockLocalStorage . length as any ) = 0 ;
137
+ // Simulate empty storage by setting length to 0
138
+ mockLocalStorage . _length = 0 ;
128
139
129
- await expect (
130
- localStorageEngineWithMock . setContentsJsonString ( 'test-config' )
131
- ) . rejects . toThrow ( StorageFullUnableToWrite ) ;
140
+ await expect ( localStorageEngineWithMock . setContentsJsonString ( 'test-config' ) ) . rejects . toThrow (
141
+ StorageFullUnableToWrite ,
142
+ ) ;
132
143
} ) ;
133
144
134
145
it ( 'should throw StorageFullUnableToWrite when setMetaJsonString fails after clear and retry' , async ( ) => {
135
146
const quotaError = new DOMException ( 'QuotaExceededError' , 'QuotaExceededError' ) ;
136
147
Object . defineProperty ( quotaError , 'code' , { value : DOMException . QUOTA_EXCEEDED_ERR } ) ;
137
-
148
+
138
149
( mockLocalStorage . setItem as jest . Mock ) . mockImplementation ( ( ) => {
139
150
throw quotaError ;
140
151
} ) ;
141
152
( mockLocalStorage . key as jest . Mock ) . mockReturnValue ( null ) ;
142
- ( mockLocalStorage . length as any ) = 0 ;
153
+ // Simulate empty storage by setting length to 0
154
+ mockLocalStorage . _length = 0 ;
143
155
144
- await expect (
145
- localStorageEngineWithMock . setMetaJsonString ( 'test-meta' )
146
- ) . rejects . toThrow ( StorageFullUnableToWrite ) ;
156
+ await expect ( localStorageEngineWithMock . setMetaJsonString ( 'test-meta' ) ) . rejects . toThrow (
157
+ StorageFullUnableToWrite ,
158
+ ) ;
147
159
} ) ;
148
160
149
161
it ( 'should succeed after clearing when retry works' , async ( ) => {
150
162
const quotaError = new DOMException ( 'QuotaExceededError' , 'QuotaExceededError' ) ;
151
163
Object . defineProperty ( quotaError , 'code' , { value : DOMException . QUOTA_EXCEEDED_ERR } ) ;
152
-
164
+
153
165
let callCount = 0 ;
154
166
( mockLocalStorage . setItem as jest . Mock ) . mockImplementation ( ( ) => {
155
167
callCount ++ ;
@@ -159,14 +171,16 @@ describe('LocalStorageStore', () => {
159
171
// Second call succeeds
160
172
} ) ;
161
173
( mockLocalStorage . key as jest . Mock ) . mockReturnValue ( 'eppo-configuration-old' ) ;
162
- ( mockLocalStorage . length as any ) = 1 ;
174
+ // Simulate storage with one item by setting length to 1
175
+ mockLocalStorage . _length = 1 ;
163
176
164
177
await expect (
165
- localStorageEngineWithMock . setContentsJsonString ( 'test-config' )
178
+ localStorageEngineWithMock . setContentsJsonString ( 'test-config' ) ,
166
179
) . resolves . not . toThrow ( ) ;
167
-
180
+
168
181
expect ( mockLocalStorage . removeItem ) . toHaveBeenCalledWith ( 'eppo-configuration-old' ) ;
169
- expect ( mockLocalStorage . setItem ) . toHaveBeenCalledTimes ( 2 ) ;
182
+ // setItem is called 3 times: 1) migration during construction, 2) first attempt (fails), 3) retry after clearing (succeeds)
183
+ expect ( mockLocalStorage . setItem ) . toHaveBeenCalledTimes ( 3 ) ;
170
184
} ) ;
171
185
172
186
it ( 'should throw LocalStorageUnknownFailure for non-quota errors' , async ( ) => {
@@ -175,7 +189,9 @@ describe('LocalStorageStore', () => {
175
189
throw otherError ;
176
190
} ) ;
177
191
178
- const error = await localStorageEngineWithMock . setContentsJsonString ( 'test-config' ) . catch ( e => e ) ;
192
+ const error = await localStorageEngineWithMock
193
+ . setContentsJsonString ( 'test-config' )
194
+ . catch ( ( e ) => e ) ;
179
195
expect ( error ) . toBeInstanceOf ( LocalStorageUnknownFailure ) ;
180
196
expect ( error . originalError ) . toBe ( otherError ) ;
181
197
expect ( error . message ) . toContain ( 'Some other error' ) ;
0 commit comments