test: add suites for state/sessions, recentSearches, upload (+17)
Via subagent, all verified against real behavior: - state/sessions (5): fallback-session round-trip across the four cinny_* keys, missing-key → undefined for each required key, removeFallbackSession clears all. - state/recentSearches (6): addRecentSearch prepend, case-sensitive dedupe + move-to-front, trim, ignore empty/whitespace, cap at 10. - state/upload (6): the createUploadAtom reducer driven through a real jotai store — idle→loading→progress(gated)→success/error, file ref preserved. No bugs found. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
// Importing the module evaluates `atomWithStorage(..., createJSONStorage(() => localStorage))`,
|
||||
// which touches `localStorage` at load time. node has none, so install a no-op
|
||||
// mock before the import is resolved.
|
||||
(globalThis as { localStorage?: unknown }).localStorage = {
|
||||
getItem: () => null,
|
||||
setItem: () => undefined,
|
||||
removeItem: () => undefined,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line import/first
|
||||
import { addRecentSearch } from './recentSearches';
|
||||
|
||||
test('addRecentSearch prepends a new term', () => {
|
||||
assert.deepEqual(addRecentSearch(['a', 'b'], 'c'), ['c', 'a', 'b']);
|
||||
});
|
||||
|
||||
test('addRecentSearch dedupes (case-sensitive) and moves the term to the front', () => {
|
||||
assert.deepEqual(addRecentSearch(['a', 'b', 'c'], 'b'), ['b', 'a', 'c']);
|
||||
// case-sensitive: differing case is treated as a distinct term
|
||||
assert.deepEqual(addRecentSearch(['a', 'B'], 'b'), ['b', 'a', 'B']);
|
||||
});
|
||||
|
||||
test('addRecentSearch trims whitespace before storing', () => {
|
||||
assert.deepEqual(addRecentSearch(['a'], ' hello '), ['hello', 'a']);
|
||||
// dedupe compares against the trimmed value
|
||||
assert.deepEqual(addRecentSearch(['hello'], ' hello '), ['hello']);
|
||||
});
|
||||
|
||||
test('addRecentSearch ignores empty / whitespace-only terms', () => {
|
||||
assert.deepEqual(addRecentSearch(['a', 'b'], ''), ['a', 'b']);
|
||||
assert.deepEqual(addRecentSearch(['a', 'b'], ' '), ['a', 'b']);
|
||||
});
|
||||
|
||||
test('addRecentSearch caps the list at 10 entries', () => {
|
||||
const ten = Array.from({ length: 10 }, (_, i) => `t${i}`);
|
||||
const result = addRecentSearch(ten, 'new');
|
||||
assert.equal(result.length, 10);
|
||||
assert.equal(result[0], 'new');
|
||||
// the oldest entry (last) is dropped
|
||||
assert.equal(result.includes('t9'), false);
|
||||
assert.deepEqual(result.slice(1), ten.slice(0, 9));
|
||||
});
|
||||
|
||||
test('addRecentSearch on an empty history returns a single-element list', () => {
|
||||
assert.deepEqual(addRecentSearch([], 'first'), ['first']);
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { setFallbackSession, removeFallbackSession, getFallbackSession } from './sessions';
|
||||
|
||||
// The fallback-session helpers read/write specific `cinny_*` keys directly on
|
||||
// `localStorage`. node has none, so install a controllable in-memory mock per
|
||||
// case backed by a Map.
|
||||
const installStorage = (): Map<string, string> => {
|
||||
const store = new Map<string, string>();
|
||||
(globalThis as { localStorage?: unknown }).localStorage = {
|
||||
getItem: (key: string) => (store.has(key) ? store.get(key) : null),
|
||||
setItem: (key: string, value: string) => {
|
||||
store.set(key, String(value));
|
||||
},
|
||||
removeItem: (key: string) => {
|
||||
store.delete(key);
|
||||
},
|
||||
};
|
||||
return store;
|
||||
};
|
||||
|
||||
test('setFallbackSession writes the cinny_* keys', () => {
|
||||
const store = installStorage();
|
||||
setFallbackSession('token-1', 'DEVICE1', '@alice:example.org', 'https://hs.example.org');
|
||||
|
||||
assert.equal(store.get('cinny_access_token'), 'token-1');
|
||||
assert.equal(store.get('cinny_device_id'), 'DEVICE1');
|
||||
assert.equal(store.get('cinny_user_id'), '@alice:example.org');
|
||||
assert.equal(store.get('cinny_hs_base_url'), 'https://hs.example.org');
|
||||
});
|
||||
|
||||
test('getFallbackSession round-trips a full session and flags fallback stores', () => {
|
||||
installStorage();
|
||||
setFallbackSession('token-1', 'DEVICE1', '@alice:example.org', 'https://hs.example.org');
|
||||
|
||||
assert.deepEqual(getFallbackSession(), {
|
||||
baseUrl: 'https://hs.example.org',
|
||||
userId: '@alice:example.org',
|
||||
deviceId: 'DEVICE1',
|
||||
accessToken: 'token-1',
|
||||
fallbackSdkStores: true,
|
||||
});
|
||||
});
|
||||
|
||||
test('getFallbackSession returns undefined when nothing is stored', () => {
|
||||
installStorage();
|
||||
assert.equal(getFallbackSession(), undefined);
|
||||
});
|
||||
|
||||
test('getFallbackSession returns undefined when a single key is missing', () => {
|
||||
// Every one of the four keys is required; missing any one yields undefined.
|
||||
const keys = [
|
||||
'cinny_access_token',
|
||||
'cinny_device_id',
|
||||
'cinny_user_id',
|
||||
'cinny_hs_base_url',
|
||||
] as const;
|
||||
|
||||
keys.forEach((missing) => {
|
||||
installStorage();
|
||||
setFallbackSession('token-1', 'DEVICE1', '@alice:example.org', 'https://hs.example.org');
|
||||
localStorage.removeItem(missing);
|
||||
assert.equal(getFallbackSession(), undefined, `missing ${missing} should yield undefined`);
|
||||
});
|
||||
});
|
||||
|
||||
test('removeFallbackSession clears all keys', () => {
|
||||
const store = installStorage();
|
||||
setFallbackSession('token-1', 'DEVICE1', '@alice:example.org', 'https://hs.example.org');
|
||||
removeFallbackSession();
|
||||
|
||||
assert.equal(store.size, 0);
|
||||
assert.equal(getFallbackSession(), undefined);
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createStore } from 'jotai';
|
||||
import { UploadResponse, UploadProgress, MatrixError } from 'matrix-js-sdk';
|
||||
import { createUploadAtom, UploadStatus, Upload } from './upload';
|
||||
import { TUploadContent } from '../utils/matrix';
|
||||
|
||||
// We exercise the pure reducer inside `createUploadAtom` by driving it through a
|
||||
// plain jotai store. The write atom is a state machine over UploadStatus.
|
||||
//
|
||||
// SKIPPED (require a real MatrixClient and/or React render, not pure logic):
|
||||
// - useBindUploadAtom (React hook; calls mx.cancelUpload, uploadContent)
|
||||
// - createUploadAtomFamily / createUploadFamilyObserverAtom (thin atomFamily
|
||||
// wrappers whose behavior is just createUploadAtom + jotai plumbing)
|
||||
|
||||
const makeFile = (size = 100): TUploadContent => ({ size }) as unknown as TUploadContent;
|
||||
|
||||
test('createUploadAtom starts in the Idle state holding the file', () => {
|
||||
const store = createStore();
|
||||
const file = makeFile();
|
||||
const uploadAtom = createUploadAtom(file);
|
||||
|
||||
const state = store.get(uploadAtom);
|
||||
assert.equal(state.status, UploadStatus.Idle);
|
||||
assert.equal(state.file, file);
|
||||
});
|
||||
|
||||
test('a promise update transitions Idle -> Loading with zeroed progress', () => {
|
||||
const store = createStore();
|
||||
const file = makeFile(2048);
|
||||
const uploadAtom = createUploadAtom(file);
|
||||
|
||||
const promise = Promise.resolve({} as UploadResponse);
|
||||
store.set(uploadAtom, { promise });
|
||||
|
||||
const state = store.get(uploadAtom);
|
||||
assert.equal(state.status, UploadStatus.Loading);
|
||||
if (state.status === UploadStatus.Loading) {
|
||||
assert.equal(state.promise, promise);
|
||||
assert.deepEqual(state.progress, { loaded: 0, total: 2048 });
|
||||
}
|
||||
});
|
||||
|
||||
test('a progress update is applied only while Loading', () => {
|
||||
const store = createStore();
|
||||
const uploadAtom = createUploadAtom(makeFile(2048));
|
||||
|
||||
const progress: UploadProgress = { loaded: 512, total: 2048 };
|
||||
|
||||
// Ignored while Idle (not Loading).
|
||||
store.set(uploadAtom, { progress });
|
||||
assert.equal(store.get(uploadAtom).status, UploadStatus.Idle);
|
||||
|
||||
// Enter Loading, then progress sticks.
|
||||
store.set(uploadAtom, { promise: Promise.resolve({} as UploadResponse) });
|
||||
store.set(uploadAtom, { progress });
|
||||
const state = store.get(uploadAtom);
|
||||
assert.equal(state.status, UploadStatus.Loading);
|
||||
if (state.status === UploadStatus.Loading) {
|
||||
assert.deepEqual(state.progress, progress);
|
||||
}
|
||||
});
|
||||
|
||||
test('an mxc update transitions to Success', () => {
|
||||
const store = createStore();
|
||||
const uploadAtom = createUploadAtom(makeFile());
|
||||
|
||||
store.set(uploadAtom, { mxc: 'mxc://example.org/abc' });
|
||||
const state = store.get(uploadAtom);
|
||||
assert.equal(state.status, UploadStatus.Success);
|
||||
if (state.status === UploadStatus.Success) {
|
||||
assert.equal(state.mxc, 'mxc://example.org/abc');
|
||||
}
|
||||
});
|
||||
|
||||
test('an error update transitions to Error', () => {
|
||||
const store = createStore();
|
||||
const uploadAtom = createUploadAtom(makeFile());
|
||||
|
||||
const error = new Error('boom') as unknown as MatrixError;
|
||||
store.set(uploadAtom, { error });
|
||||
const state = store.get(uploadAtom);
|
||||
assert.equal(state.status, UploadStatus.Error);
|
||||
if (state.status === UploadStatus.Error) {
|
||||
assert.equal(state.error, error);
|
||||
}
|
||||
});
|
||||
|
||||
test('the file reference is preserved across transitions', () => {
|
||||
const store = createStore();
|
||||
const file = makeFile();
|
||||
const uploadAtom = createUploadAtom(file);
|
||||
|
||||
const seenFiles: TUploadContent[] = [];
|
||||
const record = (s: Upload) => seenFiles.push(s.file);
|
||||
|
||||
store.set(uploadAtom, { promise: Promise.resolve({} as UploadResponse) });
|
||||
record(store.get(uploadAtom));
|
||||
store.set(uploadAtom, { mxc: 'mxc://example.org/x' });
|
||||
record(store.get(uploadAtom));
|
||||
|
||||
assert.ok(seenFiles.every((f) => f === file));
|
||||
});
|
||||
Reference in New Issue
Block a user