Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions js/packages/wallet-adapter-mobile/test/base64Utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// @vitest-environment jsdom
import { describe, expect, it } from 'vitest';

import { fromUint8Array, toUint8Array } from '../src/base64Utils.js';

const BYTES = Uint8Array.of(0, 1, 2, 253, 254, 255);

describe('base64Utils', () => {
it('encodes bytes to a base64 string', () => {
expect(fromUint8Array(BYTES)).toBe('AAEC/f7/');
});

it('decodes a base64 string to bytes', () => {
expect(toUint8Array('AAEC/f7/')).toEqual(BYTES);
});

it('round-trips bytes through the browser helpers', () => {
expect(toUint8Array(fromUint8Array(BYTES))).toEqual(BYTES);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';

import createDefaultAddressSelector from '../src/createDefaultAddressSelector.js';

describe('createDefaultAddressSelector', () => {
it('falls back to the first address when there are multiple options', async () => {
const selector = createDefaultAddressSelector();

await expect(selector.select(['first-address', 'second-address'])).resolves.toBe('first-address');
});

it('returns the only available address', async () => {
const selector = createDefaultAddressSelector();

await expect(selector.select(['only-address'])).resolves.toBe('only-address');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// @vitest-environment jsdom

import { type Authorization } from '@solana-mobile/wallet-standard-mobile';
import base58 from 'bs58';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import createDefaultAuthorizationResultCache from '../src/createDefaultAuthorizationResultCache.js';

const AUTHORIZATION_CACHE_KEY = 'SolanaMobileWalletAdapterDefaultAuthorizationCache';
const SOLANA_MAINNET_CHAIN = 'solana:mainnet';
const DEFAULT_CAPABILITIES: Authorization['capabilities'] = {
features: [],
max_messages_per_request: 1,
max_transactions_per_request: 1,
supported_transaction_versions: [],
supports_clone_authorization: false,
supports_sign_and_send_transactions: false,
};

beforeEach(() => {
installLocalStorage();
});

afterEach(() => {
vi.restoreAllMocks();
});

describe('createDefaultAuthorizationResultCache', () => {
it('falls back to decoding public keys from account addresses', async () => {
const publicKey = Uint8Array.of(7, 8, 9);

window.localStorage.setItem(
AUTHORIZATION_CACHE_KEY,
JSON.stringify({
accounts: [
{
address: base58.encode(publicKey),
chains: [SOLANA_MAINNET_CHAIN],
features: [],
icon: 'data:image/svg+xml;base64,icon',
label: 'Primary',
},
],
auth_token: 'token',
capabilities: DEFAULT_CAPABILITIES,
chain: SOLANA_MAINNET_CHAIN,
wallet_uri_base: 'https://example.com',
}),
);

const authorization = await createDefaultAuthorizationResultCache().get();

expectAccountPublicKey(authorization?.accounts[0], publicKey);
});

it('persists cached authorization, rehydrates serialized public keys, and clears the cache', async () => {
const publicKey = Uint8Array.of(1, 2, 3);

const authorization = createAuthorization(publicKey);
const cache = createDefaultAuthorizationResultCache();

await cache.set(authorization);

const cachedAuthorization = await cache.get();

expect(window.localStorage.getItem(AUTHORIZATION_CACHE_KEY)).not.toBeNull();
expectAccountPublicKey(cachedAuthorization?.accounts[0], publicKey);
expect(cachedAuthorization?.auth_token).toBe('token');
expect(cachedAuthorization && 'chain' in cachedAuthorization ? cachedAuthorization.chain : undefined).toBe(
SOLANA_MAINNET_CHAIN,
);

await cache.clear();

expect(window.localStorage.getItem(AUTHORIZATION_CACHE_KEY)).toBeNull();
});

it('returns undefined for invalid cached JSON', async () => {
window.localStorage.setItem(AUTHORIZATION_CACHE_KEY, '{');

await expect(createDefaultAuthorizationResultCache().get()).resolves.toBeUndefined();
});

it('returns undefined when localStorage is unavailable', async () => {
Object.defineProperty(window, 'localStorage', {
configurable: true,
get() {
throw new Error('localStorage unavailable');
},
});

await expect(createDefaultAuthorizationResultCache().get()).resolves.toBeUndefined();
});
});

function createAuthorization(publicKey: Uint8Array): Authorization {
return {
accounts: [
{
address: base58.encode(publicKey),
chains: [SOLANA_MAINNET_CHAIN],
features: [],
icon: 'data:image/svg+xml;base64,icon',
label: 'Primary',
publicKey,
},
],
auth_token: 'token',
capabilities: DEFAULT_CAPABILITIES,
chain: SOLANA_MAINNET_CHAIN,
wallet_uri_base: 'https://example.com',
};
}

function expectAccountPublicKey(account: Authorization['accounts'][number] | undefined, expectedPublicKey: Uint8Array) {
expect(account).toBeDefined();
expect(account && 'publicKey' in account).toBe(true);
if (!account || !('publicKey' in account)) {
return;
}
expect(account.publicKey).toEqual(expectedPublicKey);
}

function installLocalStorage() {
let store = new Map<string, string>();

Object.defineProperty(window, 'localStorage', {
configurable: true,
value: {
clear() {
store = new Map();
},
getItem(key: string) {
return store.get(key) ?? null;
},
key(index: number) {
return [...store.keys()][index] ?? null;
},
get length() {
return store.size;
},
removeItem(key: string) {
store.delete(key);
},
setItem(key: string, value: string) {
store.set(key, value);
},
} satisfies Storage,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { afterEach, describe, expect, it, vi } from 'vitest';

const { mockDefaultErrorModalWalletNotFoundHandler } = vi.hoisted(() => ({
mockDefaultErrorModalWalletNotFoundHandler: vi.fn(),
}));

vi.mock('@solana-mobile/wallet-standard-mobile', () => ({
defaultErrorModalWalletNotFoundHandler: mockDefaultErrorModalWalletNotFoundHandler,
}));

import createDefaultWalletNotFoundHandler from '../src/createDefaultWalletNotFoundHandler.js';

afterEach(() => {
mockDefaultErrorModalWalletNotFoundHandler.mockReset();
});

describe('createDefaultWalletNotFoundHandler', () => {
it('returns a handler that forwards to the standard wallet-not-found handler', async () => {
mockDefaultErrorModalWalletNotFoundHandler.mockResolvedValue(undefined);

const handler = createDefaultWalletNotFoundHandler();
const mobileWalletAdapter = {} as Parameters<typeof handler>[0];

await expect(handler(mobileWalletAdapter)).resolves.toBeUndefined();
expect(mockDefaultErrorModalWalletNotFoundHandler).toHaveBeenCalledTimes(1);
expect(mockDefaultErrorModalWalletNotFoundHandler).toHaveBeenCalledWith();
});
});
7 changes: 0 additions & 7 deletions js/packages/wallet-adapter-mobile/test/dummy.test.ts

This file was deleted.

63 changes: 63 additions & 0 deletions js/packages/wallet-adapter-mobile/test/getIsSupported.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// @vitest-environment jsdom
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import getIsSupported from '../src/getIsSupported.js';

const ANDROID_BROWSER_USER_AGENT =
'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Mobile Safari/537.36';
const DESKTOP_BROWSER_USER_AGENT =
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36';

beforeEach(() => {
installBrowserGlobals();
});

afterEach(() => {
vi.restoreAllMocks();
});

describe('getIsSupported', () => {
it('returns true for secure Android browsers', () => {
installBrowserGlobals({
isSecureContext: true,
userAgent: ANDROID_BROWSER_USER_AGENT,
});

expect(getIsSupported()).toBe(true);
});

it('returns false for insecure Android browsers', () => {
installBrowserGlobals({
isSecureContext: false,
userAgent: ANDROID_BROWSER_USER_AGENT,
});

expect(getIsSupported()).toBe(false);
});

it('returns false for non-Android browsers', () => {
installBrowserGlobals({
isSecureContext: true,
userAgent: DESKTOP_BROWSER_USER_AGENT,
});

expect(getIsSupported()).toBe(false);
});
});

function installBrowserGlobals({
isSecureContext = true,
userAgent = DESKTOP_BROWSER_USER_AGENT,
}: {
isSecureContext?: boolean;
userAgent?: string;
} = {}) {
Object.defineProperty(navigator, 'userAgent', {
configurable: true,
value: userAgent,
});
Object.defineProperty(window, 'isSecureContext', {
configurable: true,
value: isSecureContext,
});
}
2 changes: 1 addition & 1 deletion js/packages/wallet-adapter-mobile/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"extends": "../../tsconfig.json",
"include": ["src"],
"include": ["src", "test"],
"compilerOptions": {
"declarationDir": "./lib/types",
"outDir": "lib/esm"
Expand Down
Loading