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:
2026-06-30 14:29:36 -04:00
parent 0bd2273bee
commit 9f4516c6a8
3 changed files with 225 additions and 0 deletions
+48
View File
@@ -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']);
});
+74
View File
@@ -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);
});
+103
View File
@@ -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));
});