Skip to content

Commit 4f8e9b5

Browse files
authored
feat(expect-webdriverio): add toHaveLocalStorageItem matcher (#1900)
* feat(to-have-local-storage-item): add expect browser local Storage utilization function * feat(to-have-local-storage-item): test toHaveLocalStorageItem * add toHaveLocalStorageItem API Docs * add tohaveLocalStorageItem types in Matchers * chore: update mathcer size in matcher size test(localStorageMatcher) * update expectedValue type
1 parent db373f7 commit 4f8e9b5

7 files changed

Lines changed: 241 additions & 1 deletion

File tree

docs/API.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,33 @@ await expect(browser).toHaveClipboardText('some clipboard text')
250250
await expect(browser).toHaveClipboardText(expect.stringContaining('clipboard text'))
251251
```
252252

253+
### toHaveLocalStorageItem
254+
255+
Checks if browser has a specific item in localStorage with an optional value.
256+
257+
##### Usage
258+
259+
```js
260+
await browser.url('https://webdriver.io/')
261+
// Check if localStorage item exists
262+
await expect(browser).toHaveLocalStorageItem('existingKey')
263+
264+
// Check localStorage item with exact value
265+
await expect(browser).toHaveLocalStorageItem('someLocalStorageKey', 'someLocalStorageValue')
266+
267+
// Check with case insensitive
268+
await expect(browser).toHaveLocalStorageItem('key', 'uppercase', { ignoreCase: true })
269+
270+
// Check with trim
271+
await expect(browser).toHaveLocalStorageItem('key', 'value', { trim: true })
272+
273+
// Check with containing
274+
await expect(browser).toHaveLocalStorageItem('key', 'long', { containing: true })
275+
276+
// Check with regex
277+
await expect(browser).toHaveLocalStorageItem('userId', /^user_\d+$/)
278+
```
279+
253280
## Element Matchers
254281

255282
### toBeDisplayed

src/matchers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './matchers/browser/toHaveClipboardText.js'
2+
export * from './matchers/browser/toHaveLocalStorageItem.js'
23
export * from './matchers/browser/toHaveTitle.js'
34
export * from './matchers/browser/toHaveUrl.js'
45
export * from './matchers/element/toBeClickable.js'
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { waitUntil, enhanceError, compareText } from '../../utils.js'
2+
import { DEFAULT_OPTIONS } from '../../constants.js'
3+
4+
export async function toHaveLocalStorageItem(
5+
browser: WebdriverIO.Browser,
6+
key: string,
7+
expectedValue?: string | RegExp | WdioAsymmetricMatcher<string>,
8+
options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS
9+
) {
10+
const isNot = this.isNot
11+
const { expectation = 'localStorage item', verb = 'have' } = this
12+
13+
await options.beforeAssertion?.({
14+
matcherName: 'toHaveLocalStorageItem',
15+
expectedValue: expectedValue ? [key, expectedValue] : key,
16+
options,
17+
})
18+
let actual
19+
const pass = await waitUntil(async () => {
20+
actual = await browser.execute((storageKey) => {
21+
return localStorage.getItem(storageKey)
22+
}, key)
23+
// if no expected value is provided, we just check if the item exists
24+
if (expectedValue === undefined) {
25+
return actual !== null
26+
}
27+
// no localStorage item found
28+
if (actual === null) {
29+
return false
30+
}
31+
return compareText(actual, expectedValue, options).result
32+
}, isNot, options)
33+
const message = enhanceError(
34+
'browser',
35+
expectedValue !== undefined ? expectedValue : `localStorage item "${key}"`,
36+
actual,
37+
this,
38+
verb,
39+
expectation,
40+
key,
41+
options
42+
)
43+
const result: ExpectWebdriverIO.AssertionResult = {
44+
pass,
45+
message: () => message
46+
}
47+
await options.afterAssertion?.({
48+
matcherName: 'toHaveLocalStorageItem',
49+
expectedValue: expectedValue ? [key, expectedValue] : key,
50+
options,
51+
result
52+
})
53+
return result
54+
}

test/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ test('index', () => {
55
expect(setOptions.name).toBe('setDefaultOptions')
66
expect(expectExport).toBeDefined()
77
expect(utils.compareText).toBeDefined()
8-
expect(matchers.size).toEqual(41)
8+
expect(matchers.size).toEqual(42)
99
})

test/matchers.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ vi.mock('@wdio/globals')
77
const ALL_MATCHERS = [
88
// browser
99
'toHaveClipboardText',
10+
'toHaveLocalStorageItem',
1011
'toHaveTitle',
1112
'toHaveUrl',
1213

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { vi, expect, describe, it, beforeEach } from 'vitest'
2+
import { browser } from '@wdio/globals'
3+
import { toHaveLocalStorageItem } from '../../../src/matchers/browser/toHaveLocalStorageItem.js'
4+
5+
vi.mock('@wdio/globals')
6+
7+
const beforeAssertion = vi.fn()
8+
const afterAssertion = vi.fn()
9+
10+
describe('toHaveLocalStorageItem', () => {
11+
beforeEach(() => {
12+
vi.clearAllMocks()
13+
})
14+
15+
it('passes when localStorage item exists with correct value', async () => {
16+
browser.execute = vi.fn().mockResolvedValue('someLocalStorageValue')
17+
18+
const result = await toHaveLocalStorageItem.call(
19+
{}, // this context
20+
browser,
21+
'someLocalStorageKey',
22+
'someLocalStorageValue',
23+
{ ignoreCase: true, beforeAssertion, afterAssertion }
24+
)
25+
26+
expect(result.pass).toBe(true)
27+
28+
// Check that browser.execute was called with correct arguments
29+
expect(browser.execute).toHaveBeenCalledWith(
30+
expect.any(Function),
31+
'someLocalStorageKey'
32+
)
33+
34+
expect(beforeAssertion).toHaveBeenCalledWith({
35+
matcherName: 'toHaveLocalStorageItem',
36+
expectedValue: ['someLocalStorageKey', 'someLocalStorageValue'],
37+
options: { ignoreCase: true, beforeAssertion, afterAssertion }
38+
})
39+
40+
expect(afterAssertion).toHaveBeenCalledWith({
41+
matcherName: 'toHaveLocalStorageItem',
42+
expectedValue: ['someLocalStorageKey', 'someLocalStorageValue'],
43+
options: { ignoreCase: true, beforeAssertion, afterAssertion },
44+
result
45+
})
46+
})
47+
48+
it('fails when localStorage item has different value', async () => {
49+
browser.execute = vi.fn().mockResolvedValue('actualValue')
50+
51+
const result = await toHaveLocalStorageItem.call(
52+
{},
53+
browser,
54+
'someKey',
55+
'expectedValue'
56+
)
57+
58+
expect(result.pass).toBe(false)
59+
})
60+
61+
it('fails when localStorage item does not exist', async () => {
62+
// Mock browser.execute to return null (item doesn't exist)
63+
browser.execute = vi.fn().mockResolvedValue(null)
64+
65+
const result = await toHaveLocalStorageItem.call(
66+
{},
67+
browser,
68+
'nonExistentKey',
69+
'someValue'
70+
)
71+
72+
expect(result.pass).toBe(false)
73+
expect(browser.execute).toHaveBeenCalledWith(
74+
expect.any(Function),
75+
'nonExistentKey'
76+
)
77+
})
78+
79+
it('passes when only checking key existence', async () => {
80+
// Mock browser.execute to return any non-null value
81+
browser.execute = vi.fn().mockResolvedValue('anyValue')
82+
83+
const result = await toHaveLocalStorageItem.call(
84+
{},
85+
browser,
86+
'existingKey'
87+
// no expectedValue parameter
88+
)
89+
90+
expect(result.pass).toBe(true)
91+
})
92+
93+
it('ignores case when ignoreCase is true', async () => {
94+
browser.execute = vi.fn().mockResolvedValue('UPPERCASE')
95+
96+
const result = await toHaveLocalStorageItem.call(
97+
{},
98+
browser,
99+
'key',
100+
'uppercase',
101+
{ ignoreCase: true }
102+
)
103+
104+
expect(result.pass).toBe(true)
105+
})
106+
107+
it('trims whitespace when trim is true', async () => {
108+
browser.execute = vi.fn().mockResolvedValue(' value ')
109+
110+
const result = await toHaveLocalStorageItem.call(
111+
{},
112+
browser,
113+
'key',
114+
'value',
115+
{ trim: true }
116+
)
117+
118+
expect(result.pass).toBe(true)
119+
})
120+
121+
it('checks containing when containing is true', async () => {
122+
browser.execute = vi.fn().mockResolvedValue('this is a long value')
123+
124+
const result = await toHaveLocalStorageItem.call(
125+
{},
126+
browser,
127+
'key',
128+
'long',
129+
{ containing: true }
130+
)
131+
132+
expect(result.pass).toBe(true)
133+
})
134+
135+
it('passes when localStorage value matches regex', async () => {
136+
browser.execute = vi.fn().mockResolvedValue('user_123')
137+
138+
const result = await toHaveLocalStorageItem.call(
139+
{},
140+
browser,
141+
'userId',
142+
/^user_\d+$/
143+
)
144+
145+
expect(result.pass).toBe(true)
146+
})
147+
})

types/expect-webdriverio.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,15 @@ interface WdioBrowserMatchers<_R, ActualT>{
8989
* `WebdriverIO.Browser` -> `execute`
9090
*/
9191
toHaveClipboardText: FnWhenBrowser<ActualT, (clipboardText: string | RegExp | ExpectWebdriverIO.PartialMatcher<string>, options?: ExpectWebdriverIO.StringOptions) => Promise<void>>
92+
93+
/**
94+
* `WebdriverIO.Browser` -> `execute`
95+
*/
96+
toHaveLocalStorageItem: FnWhenBrowser<ActualT, (
97+
key: string,
98+
expectedValue?: string | RegExp | ExpectWebdriverIO.PartialMatcher<string>,
99+
options?: ExpectWebdriverIO.StringOptions
100+
) => Promise<void>>
92101
}
93102

94103
/**
@@ -728,6 +737,7 @@ declare namespace ExpectWebdriverIO {
728737
* `true` to check if the element is invisible due to the value of its visibility property. `true` by default.
729738
* @default true
730739
*/
740+
731741
visibilityProperty?: boolean
732742
}
733743

0 commit comments

Comments
 (0)