From b62e2dbec56ba5f33b0130b895b00b4b4c025ba1 Mon Sep 17 00:00:00 2001 From: naamukim Date: Wed, 30 Jul 2025 22:31:47 +0900 Subject: [PATCH 1/4] feat(to-have-local-storage-item): add expect browser local Storage utilization function --- src/matchers.ts | 1 + .../browser/toHaveLocalStorageItem.ts | 54 +++++++ .../browser/toHaveLocalStorageItem.test.ts | 147 ++++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 src/matchers/browser/toHaveLocalStorageItem.ts create mode 100644 test/matchers/browser/toHaveLocalStorageItem.test.ts diff --git a/src/matchers.ts b/src/matchers.ts index 323fafc05..beee212be 100644 --- a/src/matchers.ts +++ b/src/matchers.ts @@ -1,4 +1,5 @@ export * from './matchers/browser/toHaveClipboardText.js' +export * from './matchers/browser/toHaveLocalStorageItem.js' export * from './matchers/browser/toHaveTitle.js' export * from './matchers/browser/toHaveUrl.js' export * from './matchers/element/toBeClickable.js' diff --git a/src/matchers/browser/toHaveLocalStorageItem.ts b/src/matchers/browser/toHaveLocalStorageItem.ts new file mode 100644 index 000000000..a05096d88 --- /dev/null +++ b/src/matchers/browser/toHaveLocalStorageItem.ts @@ -0,0 +1,54 @@ +import { waitUntil, enhanceError, compareText } from '../../utils.js' +import { DEFAULT_OPTIONS } from '../../constants.js' + +export async function toHaveLocalStorageItem( + browser: WebdriverIO.Browser, + key: string, + expectedValue?: string | RegExp | ExpectWebdriverIO.PartialMatcher, + options: ExpectWebdriverIO.StringOptions = DEFAULT_OPTIONS +) { + const isNot = this.isNot + const { expectation = 'localStorage item', verb = 'have' } = this + + await options.beforeAssertion?.({ + matcherName: 'toHaveLocalStorageItem', + expectedValue: expectedValue ? [key, expectedValue] : key, + options, + }) + let actual + const pass = await waitUntil(async () => { + actual = await browser.execute((storageKey) => { + return localStorage.getItem(storageKey) + }, key) + // if no expected value is provided, we just check if the item exists + if (expectedValue === undefined) { + return actual !== null + } + // no localStorage item found + if (actual === null) { + return false + } + return compareText(actual, expectedValue, options).result + }, isNot, options) + const message = enhanceError( + 'browser', + expectedValue !== undefined ? expectedValue : `localStorage item "${key}"`, + actual, + this, + verb, + expectation, + key, + options + ) + const result: ExpectWebdriverIO.AssertionResult = { + pass, + message: () => message + } + await options.afterAssertion?.({ + matcherName: 'toHaveLocalStorageItem', + expectedValue: expectedValue ? [key, expectedValue] : key, + options, + result + }) + return result +} \ No newline at end of file diff --git a/test/matchers/browser/toHaveLocalStorageItem.test.ts b/test/matchers/browser/toHaveLocalStorageItem.test.ts new file mode 100644 index 000000000..4499c9985 --- /dev/null +++ b/test/matchers/browser/toHaveLocalStorageItem.test.ts @@ -0,0 +1,147 @@ +import { vi, expect, describe, it, beforeEach } from 'vitest' +import { browser } from '@wdio/globals' +import { toHaveLocalStorageItem } from '../../../src/matchers/browser/toHaveLocalStorageItem.js' + +vi.mock('@wdio/globals') + +const beforeAssertion = vi.fn() +const afterAssertion = vi.fn() + +describe('toHaveLocalStorageItem', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('passes when localStorage item exists with correct value', async () => { + browser.execute = vi.fn().mockResolvedValue('someLocalStorageValue') + + const result = await toHaveLocalStorageItem.call( + {}, // this context + browser, + 'someLocalStorageKey', + 'someLocalStorageValue', + { ignoreCase: true, beforeAssertion, afterAssertion } + ) + + expect(result.pass).toBe(true) + + // Check that browser.execute was called with correct arguments + expect(browser.execute).toHaveBeenCalledWith( + expect.any(Function), + 'someLocalStorageKey' + ) + + expect(beforeAssertion).toHaveBeenCalledWith({ + matcherName: 'toHaveLocalStorageItem', + expectedValue: ['someLocalStorageKey', 'someLocalStorageValue'], + options: { ignoreCase: true, beforeAssertion, afterAssertion } + }) + + expect(afterAssertion).toHaveBeenCalledWith({ + matcherName: 'toHaveLocalStorageItem', + expectedValue: ['someLocalStorageKey', 'someLocalStorageValue'], + options: { ignoreCase: true, beforeAssertion, afterAssertion }, + result + }) + }) + + it('fails when localStorage item has different value', async () => { + browser.execute = vi.fn().mockResolvedValue('actualValue') + + const result = await toHaveLocalStorageItem.call( + {}, + browser, + 'someKey', + 'expectedValue' + ) + + expect(result.pass).toBe(false) + }) + + it('fails when localStorage item does not exist', async () => { + // Mock browser.execute to return null (item doesn't exist) + browser.execute = vi.fn().mockResolvedValue(null) + + const result = await toHaveLocalStorageItem.call( + {}, + browser, + 'nonExistentKey', + 'someValue' + ) + + expect(result.pass).toBe(false) + expect(browser.execute).toHaveBeenCalledWith( + expect.any(Function), + 'nonExistentKey' + ) + }) + + it('passes when only checking key existence', async () => { + // Mock browser.execute to return any non-null value + browser.execute = vi.fn().mockResolvedValue('anyValue') + + const result = await toHaveLocalStorageItem.call( + {}, + browser, + 'existingKey' + // no expectedValue parameter + ) + + expect(result.pass).toBe(true) + }) + + it('ignores case when ignoreCase is true', async () => { + browser.execute = vi.fn().mockResolvedValue('UPPERCASE') + + const result = await toHaveLocalStorageItem.call( + {}, + browser, + 'key', + 'uppercase', + { ignoreCase: true } + ) + + expect(result.pass).toBe(true) + }) + + it('trims whitespace when trim is true', async () => { + browser.execute = vi.fn().mockResolvedValue(' value ') + + const result = await toHaveLocalStorageItem.call( + {}, + browser, + 'key', + 'value', + { trim: true } + ) + + expect(result.pass).toBe(true) + }) + + it('checks containing when containing is true', async () => { + browser.execute = vi.fn().mockResolvedValue('this is a long value') + + const result = await toHaveLocalStorageItem.call( + {}, + browser, + 'key', + 'long', + { containing: true } + ) + + expect(result.pass).toBe(true) + }) + + it('passes when localStorage value matches regex', async () => { + browser.execute = vi.fn().mockResolvedValue('user_123') + + const result = await toHaveLocalStorageItem.call( + {}, + browser, + 'userId', + /^user_\d+$/ + ) + + expect(result.pass).toBe(true) + }) +}) \ No newline at end of file From 5550d9fb64218130c0252c14e7089525cf710e4b Mon Sep 17 00:00:00 2001 From: naamukim Date: Wed, 30 Jul 2025 22:31:52 +0900 Subject: [PATCH 2/4] feat(to-have-local-storage-item): test toHaveLocalStorageItem --- test/matchers.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/matchers.test.ts b/test/matchers.test.ts index 5e5f15ab8..0bbee4eb6 100644 --- a/test/matchers.test.ts +++ b/test/matchers.test.ts @@ -4,6 +4,7 @@ import { matchers, expect as expectLib } from '../src/index.js' const ALL_MATCHERS = [ // browser 'toHaveClipboardText', + 'toHaveLocalStorageItem', 'toHaveTitle', 'toHaveUrl', From 17b882dd760eec3d1317d92ae5425ec234b15974 Mon Sep 17 00:00:00 2001 From: naamukim Date: Tue, 25 Nov 2025 21:41:30 +0900 Subject: [PATCH 3/4] add toHaveLocalStorageItem API Docs --- docs/API.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/docs/API.md b/docs/API.md index 802927525..9806f0ac7 100644 --- a/docs/API.md +++ b/docs/API.md @@ -245,6 +245,33 @@ await expect(browser).toHaveClipboardText('some clipboard text') await expect(browser).toHaveClipboardText(expect.stringContaining('clipboard text')) ``` +### toHaveLocalStorageItem + +Checks if browser has a specific item in localStorage with an optional value. + +##### Usage + +```js +await browser.url('https://webdriver.io/') +// Check if localStorage item exists +await expect(browser).toHaveLocalStorageItem('existingKey') + +// Check localStorage item with exact value +await expect(browser).toHaveLocalStorageItem('someLocalStorageKey', 'someLocalStorageValue') + +// Check with case insensitive +await expect(browser).toHaveLocalStorageItem('key', 'uppercase', { ignoreCase: true }) + +// Check with trim +await expect(browser).toHaveLocalStorageItem('key', 'value', { trim: true }) + +// Check with containing +await expect(browser).toHaveLocalStorageItem('key', 'long', { containing: true }) + +// Check with regex +await expect(browser).toHaveLocalStorageItem('userId', /^user_\d+$/) +``` + ## Element Matchers ### toBeDisplayed From c2f28740dc4f0375f7acd38be4aacf24bcdee280 Mon Sep 17 00:00:00 2001 From: naamukim Date: Sun, 30 Nov 2025 13:45:38 +0900 Subject: [PATCH 4/4] add tohaveLocalStorageItem types in Matchers --- types/expect-webdriverio.d.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/types/expect-webdriverio.d.ts b/types/expect-webdriverio.d.ts index 40e535524..5040a09bc 100644 --- a/types/expect-webdriverio.d.ts +++ b/types/expect-webdriverio.d.ts @@ -418,7 +418,13 @@ declare namespace ExpectWebdriverIO { */ toHaveClipboardText(clipboardText: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): R - // ===== $$ only ===== + /** + * `WebdriverIO.Browser` -> `execute` + * Checks if a localStorage item exists and optionally validates its value + */ + toHaveLocalStorageItem(key: string, expectedValue?: string | RegExp | ExpectWebdriverIO.PartialMatcher, options?: ExpectWebdriverIO.StringOptions): R + + // ===== $ only ===== /** * `WebdriverIO.ElementArray` -> `$$('...').length` * supports less / greater then or equals to be passed in options