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,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