diff --git a/src/matchers.ts b/src/matchers.ts index 323fafc0..beee212b 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 00000000..a05096d8 --- /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.test.ts b/test/matchers.test.ts index 5e5f15ab..0bbee4eb 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', diff --git a/test/matchers/browser/toHaveLocalStorageItem.test.ts b/test/matchers/browser/toHaveLocalStorageItem.test.ts new file mode 100644 index 00000000..4499c998 --- /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