forked from boos/BoosAPI
feat(tests): add comprehensive tests for TokenRouter and related components
- Implement tests for TokenRouter downstream policy, covering allowedRouteIds and site weight multipliers. - Add tests for TokenRouter patterns and model mapping, including regex pattern matching and model mapping precedence. - Create tests for BrandIcon component to verify brand detection logic. - Develop tests for Accounts page, focusing on rebind panel behavior and refresh action on error. - Introduce fuzzy search utility tests to validate substring and subsequence matching. - Implement navigation focus helpers tests for building and parsing focus paths. - Add progressive render tests to ensure correct initial and next visible counts. - Create route model candidates index tests to validate route candidate indexing and matching logic.
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { fuzzyMatch } from './fuzzySearch.js';
|
||||
|
||||
describe('fuzzyMatch', () => {
|
||||
it('matches plain substring case-insensitively', () => {
|
||||
expect(fuzzyMatch('claude-opus-4-6', 'OPUS')).toBe(true);
|
||||
expect(fuzzyMatch('gpt-4o-mini', 'gpt')).toBe(true);
|
||||
});
|
||||
|
||||
it('matches after removing separators', () => {
|
||||
expect(fuzzyMatch('gpt-5.2-codex', 'gpt52')).toBe(true);
|
||||
expect(fuzzyMatch('re:^claude-(opus|sonnet)-4-5$', 'claude45')).toBe(true);
|
||||
});
|
||||
|
||||
it('supports subsequence fuzzy matching', () => {
|
||||
expect(fuzzyMatch('cerebras-llama-3.1-8b', 'cll318')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when query cannot be matched', () => {
|
||||
expect(fuzzyMatch('claude-opus-4-6', 'gemini')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
function normalize(value: string): string {
|
||||
return (value || '').toLowerCase().trim();
|
||||
}
|
||||
|
||||
function compact(value: string): string {
|
||||
return normalize(value).replace(/[^a-z0-9]/g, '');
|
||||
}
|
||||
|
||||
function isSubsequence(text: string, query: string): boolean {
|
||||
if (!query) return true;
|
||||
let qi = 0;
|
||||
for (let i = 0; i < text.length; i += 1) {
|
||||
if (text[i] === query[qi]) qi += 1;
|
||||
if (qi >= query.length) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function fuzzyMatch(text: string, query: string): boolean {
|
||||
const q = normalize(query);
|
||||
if (!q) return true;
|
||||
|
||||
const t = normalize(text);
|
||||
if (t.includes(q)) return true;
|
||||
|
||||
const compactText = compact(t);
|
||||
const compactQuery = compact(q);
|
||||
if (!compactQuery) return true;
|
||||
if (compactText.includes(compactQuery)) return true;
|
||||
|
||||
return isSubsequence(compactText, compactQuery);
|
||||
}
|
||||
@@ -125,7 +125,12 @@ const toNullableFiniteNumber = (value: unknown): number | null => {
|
||||
return null;
|
||||
};
|
||||
|
||||
const isExactModelPattern = (modelPattern: string): boolean => !/[\*\?\[]/.test(modelPattern);
|
||||
const isExactModelPattern = (modelPattern: string): boolean => {
|
||||
const normalized = modelPattern.trim();
|
||||
if (!normalized) return false;
|
||||
if (normalized.toLowerCase().startsWith('re:')) return false;
|
||||
return !/[\*\?\[]/.test(normalized);
|
||||
};
|
||||
|
||||
export const collectModelTesterModelNames = (
|
||||
marketplace: { models?: Array<{ name?: unknown }>; } | null | undefined,
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
buildAccountFocusPath,
|
||||
buildEventNavigationPath,
|
||||
buildSiteFocusPath,
|
||||
clearFocusParams,
|
||||
readFocusAccountIntent,
|
||||
readFocusSiteId,
|
||||
} from './navigationFocus.js';
|
||||
|
||||
describe('navigationFocus helpers', () => {
|
||||
it('builds focus paths for site and account', () => {
|
||||
expect(buildSiteFocusPath(12)).toBe('/sites?focusSiteId=12');
|
||||
expect(buildSiteFocusPath(0)).toBe('/sites');
|
||||
expect(buildAccountFocusPath(34)).toBe('/accounts?focusAccountId=34');
|
||||
expect(buildAccountFocusPath(34, { openRebind: true })).toBe('/accounts?focusAccountId=34&openRebind=1');
|
||||
expect(buildAccountFocusPath(-1)).toBe('/accounts');
|
||||
});
|
||||
|
||||
it('parses focus params from query string', () => {
|
||||
expect(readFocusSiteId('?focusSiteId=15')).toBe(15);
|
||||
expect(readFocusSiteId('?focusSiteId=abc')).toBeNull();
|
||||
|
||||
expect(readFocusAccountIntent('?focusAccountId=22&openRebind=1')).toEqual({
|
||||
accountId: 22,
|
||||
openRebind: true,
|
||||
});
|
||||
expect(readFocusAccountIntent('?focusAccountId=22&openRebind=true')).toEqual({
|
||||
accountId: 22,
|
||||
openRebind: true,
|
||||
});
|
||||
expect(readFocusAccountIntent('?focusAccountId=22')).toEqual({
|
||||
accountId: 22,
|
||||
openRebind: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('clears focus params but keeps other params', () => {
|
||||
expect(clearFocusParams('?focusSiteId=12&q=abc')).toBe('?q=abc');
|
||||
expect(clearFocusParams('?focusAccountId=2&openRebind=1&type=token')).toBe('?type=token');
|
||||
expect(clearFocusParams('?focusAccountId=2')).toBe('');
|
||||
});
|
||||
|
||||
it('builds event navigation path by related entity', () => {
|
||||
expect(buildEventNavigationPath({
|
||||
relatedType: 'account',
|
||||
relatedId: 18,
|
||||
type: 'token',
|
||||
})).toBe('/accounts?focusAccountId=18&openRebind=1');
|
||||
|
||||
expect(buildEventNavigationPath({
|
||||
relatedType: 'account',
|
||||
relatedId: 18,
|
||||
type: 'checkin',
|
||||
})).toBe('/accounts?focusAccountId=18');
|
||||
|
||||
expect(buildEventNavigationPath({
|
||||
relatedType: 'site',
|
||||
relatedId: 9,
|
||||
type: 'status',
|
||||
})).toBe('/sites?focusSiteId=9');
|
||||
|
||||
expect(buildEventNavigationPath({
|
||||
relatedType: 'route',
|
||||
relatedId: null,
|
||||
type: 'proxy',
|
||||
})).toBe('/routes');
|
||||
|
||||
expect(buildEventNavigationPath({ type: 'proxy' })).toBe('/logs');
|
||||
expect(buildEventNavigationPath({ type: 'checkin' })).toBe('/checkin');
|
||||
expect(buildEventNavigationPath({})).toBe('/events');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
const FOCUS_SITE_ID_KEY = 'focusSiteId';
|
||||
const FOCUS_ACCOUNT_ID_KEY = 'focusAccountId';
|
||||
const OPEN_REBIND_KEY = 'openRebind';
|
||||
|
||||
function normalizePositiveId(input: unknown): number | null {
|
||||
const value = Number.parseInt(String(input ?? ''), 10);
|
||||
if (!Number.isFinite(value) || value <= 0) return null;
|
||||
return value;
|
||||
}
|
||||
|
||||
function isTruthyFlag(value: string | null): boolean {
|
||||
if (!value) return false;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized === '1' || normalized === 'true' || normalized === 'yes';
|
||||
}
|
||||
|
||||
export function buildSiteFocusPath(siteId: number): string {
|
||||
const normalizedId = normalizePositiveId(siteId);
|
||||
if (!normalizedId) return '/sites';
|
||||
return `/sites?${FOCUS_SITE_ID_KEY}=${normalizedId}`;
|
||||
}
|
||||
|
||||
export function buildAccountFocusPath(
|
||||
accountId: number,
|
||||
options?: { openRebind?: boolean },
|
||||
): string {
|
||||
const normalizedId = normalizePositiveId(accountId);
|
||||
if (!normalizedId) return '/accounts';
|
||||
const params = new URLSearchParams();
|
||||
params.set(FOCUS_ACCOUNT_ID_KEY, String(normalizedId));
|
||||
if (options?.openRebind) params.set(OPEN_REBIND_KEY, '1');
|
||||
return `/accounts?${params.toString()}`;
|
||||
}
|
||||
|
||||
export function readFocusSiteId(search: string): number | null {
|
||||
const params = new URLSearchParams(search);
|
||||
return normalizePositiveId(params.get(FOCUS_SITE_ID_KEY));
|
||||
}
|
||||
|
||||
export function readFocusAccountIntent(search: string): { accountId: number | null; openRebind: boolean } {
|
||||
const params = new URLSearchParams(search);
|
||||
return {
|
||||
accountId: normalizePositiveId(params.get(FOCUS_ACCOUNT_ID_KEY)),
|
||||
openRebind: isTruthyFlag(params.get(OPEN_REBIND_KEY)),
|
||||
};
|
||||
}
|
||||
|
||||
export function clearFocusParams(search: string): string {
|
||||
const params = new URLSearchParams(search);
|
||||
params.delete(FOCUS_SITE_ID_KEY);
|
||||
params.delete(FOCUS_ACCOUNT_ID_KEY);
|
||||
params.delete(OPEN_REBIND_KEY);
|
||||
const next = params.toString();
|
||||
return next ? `?${next}` : '';
|
||||
}
|
||||
|
||||
export function buildEventNavigationPath(event: {
|
||||
relatedType?: string | null;
|
||||
relatedId?: number | null;
|
||||
type?: string | null;
|
||||
}): string {
|
||||
const relatedType = (event.relatedType || '').toLowerCase();
|
||||
const relatedId = normalizePositiveId(event.relatedId);
|
||||
const eventType = (event.type || '').toLowerCase();
|
||||
|
||||
if (relatedType === 'account' && relatedId) {
|
||||
return buildAccountFocusPath(relatedId, { openRebind: eventType === 'token' });
|
||||
}
|
||||
if (relatedType === 'site' && relatedId) {
|
||||
return buildSiteFocusPath(relatedId);
|
||||
}
|
||||
if (relatedType === 'route') {
|
||||
return '/routes';
|
||||
}
|
||||
if (eventType === 'proxy') {
|
||||
return '/logs';
|
||||
}
|
||||
if (eventType === 'checkin') {
|
||||
return '/checkin';
|
||||
}
|
||||
|
||||
return '/events';
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getInitialVisibleCount, getNextVisibleCount } from './progressiveRender.js';
|
||||
|
||||
describe('progressiveRender', () => {
|
||||
it('returns bounded initial visible count', () => {
|
||||
expect(getInitialVisibleCount(0, 40)).toBe(0);
|
||||
expect(getInitialVisibleCount(10, 40)).toBe(10);
|
||||
expect(getInitialVisibleCount(120, 40)).toBe(40);
|
||||
});
|
||||
|
||||
it('returns next visible count without exceeding total', () => {
|
||||
expect(getNextVisibleCount(0, 120, 40)).toBe(40);
|
||||
expect(getNextVisibleCount(40, 120, 40)).toBe(80);
|
||||
expect(getNextVisibleCount(80, 120, 40)).toBe(120);
|
||||
expect(getNextVisibleCount(200, 120, 40)).toBe(120);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
export function getInitialVisibleCount(total: number, chunkSize: number): number {
|
||||
if (!Number.isFinite(total) || total <= 0) return 0;
|
||||
const normalizedChunk = Number.isFinite(chunkSize) && chunkSize > 0 ? Math.trunc(chunkSize) : 1;
|
||||
return Math.min(Math.trunc(total), normalizedChunk);
|
||||
}
|
||||
|
||||
export function getNextVisibleCount(currentVisible: number, total: number, chunkSize: number): number {
|
||||
const normalizedTotal = Number.isFinite(total) && total > 0 ? Math.trunc(total) : 0;
|
||||
if (normalizedTotal <= 0) return 0;
|
||||
|
||||
const normalizedCurrent = Number.isFinite(currentVisible) && currentVisible > 0
|
||||
? Math.trunc(currentVisible)
|
||||
: 0;
|
||||
const normalizedChunk = Number.isFinite(chunkSize) && chunkSize > 0 ? Math.trunc(chunkSize) : 1;
|
||||
|
||||
return Math.min(normalizedTotal, normalizedCurrent + normalizedChunk);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
buildRouteModelCandidatesIndex,
|
||||
type IndexedRouteModelCandidate,
|
||||
type RouteModelCandidatesByModelName,
|
||||
} from './routeModelCandidatesIndex.js';
|
||||
|
||||
function matchesModelPattern(model: string, pattern: string): boolean {
|
||||
if (!pattern) return false;
|
||||
if (pattern === '*') return true;
|
||||
if (pattern.endsWith('*')) return model.startsWith(pattern.slice(0, -1));
|
||||
return model === pattern;
|
||||
}
|
||||
|
||||
describe('buildRouteModelCandidatesIndex', () => {
|
||||
it('precomputes route candidates, account options and token options by account', () => {
|
||||
const routes = [
|
||||
{ id: 1, modelPattern: 'claude-*' },
|
||||
{ id: 2, modelPattern: 'gpt-4o-mini' },
|
||||
];
|
||||
|
||||
const modelCandidates: RouteModelCandidatesByModelName = {
|
||||
'claude-opus-4-6': [
|
||||
{
|
||||
modelName: 'will-be-overwritten',
|
||||
accountId: 11,
|
||||
tokenId: 101,
|
||||
tokenName: 'tk-opus',
|
||||
isDefault: false,
|
||||
username: 'alice',
|
||||
siteId: 1,
|
||||
siteName: 'site-a',
|
||||
},
|
||||
{
|
||||
modelName: 'dup',
|
||||
accountId: 11,
|
||||
tokenId: 101,
|
||||
tokenName: 'tk-opus',
|
||||
isDefault: false,
|
||||
username: 'alice',
|
||||
siteId: 1,
|
||||
siteName: 'site-a',
|
||||
},
|
||||
],
|
||||
'claude-sonnet-4-6': [
|
||||
{
|
||||
modelName: 'will-be-overwritten',
|
||||
accountId: 11,
|
||||
tokenId: 102,
|
||||
tokenName: 'tk-sonnet',
|
||||
isDefault: true,
|
||||
username: 'alice',
|
||||
siteId: 1,
|
||||
siteName: 'site-a',
|
||||
},
|
||||
],
|
||||
'gpt-4o-mini': [
|
||||
{
|
||||
modelName: 'will-be-overwritten',
|
||||
accountId: 22,
|
||||
tokenId: 201,
|
||||
tokenName: 'tk-gpt',
|
||||
isDefault: true,
|
||||
username: 'bob',
|
||||
siteId: 2,
|
||||
siteName: 'site-b',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const index = buildRouteModelCandidatesIndex(routes, modelCandidates, matchesModelPattern);
|
||||
|
||||
expect(index[1]).toBeTruthy();
|
||||
expect(index[2]).toBeTruthy();
|
||||
|
||||
const route1Candidates = index[1].routeCandidates;
|
||||
expect(route1Candidates).toHaveLength(2);
|
||||
expect(route1Candidates.map((item) => item.modelName).sort()).toEqual([
|
||||
'claude-opus-4-6',
|
||||
'claude-sonnet-4-6',
|
||||
]);
|
||||
|
||||
expect(index[1].accountOptions).toEqual([{ id: 11, label: 'alice @ site-a' }]);
|
||||
|
||||
const route1TokenOptions = index[1].tokenOptionsByAccountId[11];
|
||||
expect(route1TokenOptions.map((item) => `${item.id}:${item.sourceModel}:${item.isDefault}`)).toEqual([
|
||||
'102:claude-sonnet-4-6:true',
|
||||
'101:claude-opus-4-6:false',
|
||||
]);
|
||||
|
||||
expect(index[2].routeCandidates).toHaveLength(1);
|
||||
expect(index[2].routeCandidates[0].modelName).toBe('gpt-4o-mini');
|
||||
expect(index[2].accountOptions).toEqual([{ id: 22, label: 'bob @ site-b' }]);
|
||||
});
|
||||
|
||||
it('returns empty structures when route model pattern is blank', () => {
|
||||
const routes = [{ id: 1, modelPattern: ' ' }];
|
||||
const modelCandidates: RouteModelCandidatesByModelName = {
|
||||
'gpt-4o-mini': [{
|
||||
modelName: 'ignored',
|
||||
accountId: 1,
|
||||
tokenId: 2,
|
||||
tokenName: 'tk',
|
||||
isDefault: false,
|
||||
username: null,
|
||||
siteId: 1,
|
||||
siteName: 'site',
|
||||
} satisfies IndexedRouteModelCandidate],
|
||||
};
|
||||
|
||||
const index = buildRouteModelCandidatesIndex(routes, modelCandidates, matchesModelPattern);
|
||||
expect(index[1].routeCandidates).toEqual([]);
|
||||
expect(index[1].accountOptions).toEqual([]);
|
||||
expect(index[1].tokenOptionsByAccountId).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
export type IndexedRouteModelCandidate = {
|
||||
modelName: string;
|
||||
accountId: number;
|
||||
tokenId: number;
|
||||
tokenName: string;
|
||||
isDefault: boolean;
|
||||
username: string | null;
|
||||
siteId: number;
|
||||
siteName: string;
|
||||
};
|
||||
|
||||
export type RouteModelCandidatesByModelName = Record<string, IndexedRouteModelCandidate[]>;
|
||||
|
||||
export type RouteAccountOption = {
|
||||
id: number;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type RouteTokenOption = {
|
||||
id: number;
|
||||
name: string;
|
||||
isDefault: boolean;
|
||||
sourceModel?: string;
|
||||
};
|
||||
|
||||
export type RouteCandidateView = {
|
||||
routeCandidates: IndexedRouteModelCandidate[];
|
||||
accountOptions: RouteAccountOption[];
|
||||
tokenOptionsByAccountId: Record<number, RouteTokenOption[]>;
|
||||
};
|
||||
|
||||
export type RouteModelPatternLike = {
|
||||
id: number;
|
||||
modelPattern: string;
|
||||
};
|
||||
|
||||
const EMPTY_ROUTE_CANDIDATE_VIEW: RouteCandidateView = {
|
||||
routeCandidates: [],
|
||||
accountOptions: [],
|
||||
tokenOptionsByAccountId: {},
|
||||
};
|
||||
|
||||
export function buildRouteModelCandidatesIndex(
|
||||
routes: RouteModelPatternLike[],
|
||||
modelCandidates: RouteModelCandidatesByModelName,
|
||||
matchesModelPattern: (model: string, pattern: string) => boolean,
|
||||
): Record<number, RouteCandidateView> {
|
||||
const index: Record<number, RouteCandidateView> = {};
|
||||
|
||||
for (const route of routes || []) {
|
||||
const modelPattern = (route.modelPattern || '').trim();
|
||||
if (!modelPattern) {
|
||||
index[route.id] = EMPTY_ROUTE_CANDIDATE_VIEW;
|
||||
continue;
|
||||
}
|
||||
|
||||
const deduped = new Map<string, IndexedRouteModelCandidate>();
|
||||
for (const [modelName, candidates] of Object.entries(modelCandidates || {})) {
|
||||
if (!matchesModelPattern(modelName, modelPattern)) continue;
|
||||
for (const candidate of candidates || []) {
|
||||
const key = `${candidate.tokenId}::${modelName}`;
|
||||
if (!deduped.has(key)) {
|
||||
deduped.set(key, {
|
||||
...candidate,
|
||||
modelName,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const routeCandidates = Array.from(deduped.values()).sort((a, b) => {
|
||||
if (a.accountId === b.accountId) return a.tokenId - b.tokenId;
|
||||
return a.accountId - b.accountId;
|
||||
});
|
||||
|
||||
const accountMap = new Map<number, string>();
|
||||
const tokenOptionsByAccountId: Record<number, RouteTokenOption[]> = {};
|
||||
for (const candidate of routeCandidates) {
|
||||
if (!accountMap.has(candidate.accountId)) {
|
||||
accountMap.set(candidate.accountId, `${candidate.username || `account-${candidate.accountId}`} @ ${candidate.siteName}`);
|
||||
}
|
||||
if (!tokenOptionsByAccountId[candidate.accountId]) {
|
||||
tokenOptionsByAccountId[candidate.accountId] = [];
|
||||
}
|
||||
tokenOptionsByAccountId[candidate.accountId].push({
|
||||
id: candidate.tokenId,
|
||||
name: candidate.tokenName,
|
||||
isDefault: candidate.isDefault,
|
||||
sourceModel: candidate.modelName,
|
||||
});
|
||||
}
|
||||
|
||||
for (const accountIdText of Object.keys(tokenOptionsByAccountId)) {
|
||||
const accountId = Number.parseInt(accountIdText, 10);
|
||||
tokenOptionsByAccountId[accountId].sort((a, b) => {
|
||||
if (a.isDefault === b.isDefault) {
|
||||
if (a.id === b.id) return (a.sourceModel || '').localeCompare(b.sourceModel || '', undefined, { sensitivity: 'base' });
|
||||
return a.id - b.id;
|
||||
}
|
||||
return a.isDefault ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
const accountOptions = Array.from(accountMap.entries()).map(([id, label]) => ({ id, label }));
|
||||
index[route.id] = {
|
||||
routeCandidates,
|
||||
accountOptions,
|
||||
tokenOptionsByAccountId,
|
||||
};
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
@@ -5,7 +5,13 @@ describe('buildSiteSaveAction', () => {
|
||||
it('returns add action in add mode', () => {
|
||||
const action = buildSiteSaveAction(
|
||||
{ mode: 'add' },
|
||||
{ name: 'site-a', url: 'https://a.example.com/', platform: 'new-api', apiKey: 'sk-1' },
|
||||
{
|
||||
name: 'site-a',
|
||||
url: 'https://a.example.com/',
|
||||
platform: 'new-api',
|
||||
apiKey: 'sk-1',
|
||||
proxyUrl: 'http://127.0.0.1:7890',
|
||||
},
|
||||
);
|
||||
|
||||
expect(action).toEqual({
|
||||
@@ -15,6 +21,7 @@ describe('buildSiteSaveAction', () => {
|
||||
url: 'https://a.example.com/',
|
||||
platform: 'new-api',
|
||||
apiKey: 'sk-1',
|
||||
proxyUrl: 'http://127.0.0.1:7890',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -22,7 +29,7 @@ describe('buildSiteSaveAction', () => {
|
||||
it('returns update action in edit mode with site id', () => {
|
||||
const action = buildSiteSaveAction(
|
||||
{ mode: 'edit', editingSiteId: 12 },
|
||||
{ name: 'site-b', url: 'https://b.example.com', platform: 'one-api', apiKey: '' },
|
||||
{ name: 'site-b', url: 'https://b.example.com', platform: 'one-api', apiKey: '', proxyUrl: '' },
|
||||
);
|
||||
|
||||
expect(action).toEqual({
|
||||
@@ -33,6 +40,7 @@ describe('buildSiteSaveAction', () => {
|
||||
url: 'https://b.example.com',
|
||||
platform: 'one-api',
|
||||
apiKey: '',
|
||||
proxyUrl: '',
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -41,7 +49,7 @@ describe('buildSiteSaveAction', () => {
|
||||
expect(() =>
|
||||
buildSiteSaveAction(
|
||||
{ mode: 'edit' },
|
||||
{ name: 'site-c', url: 'https://c.example.com', platform: '', apiKey: '' },
|
||||
{ name: 'site-c', url: 'https://c.example.com', platform: '', apiKey: '', proxyUrl: '' },
|
||||
),
|
||||
).toThrow('editingSiteId is required in edit mode');
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ export type SiteForm = {
|
||||
url: string;
|
||||
platform: string;
|
||||
apiKey: string;
|
||||
proxyUrl: string;
|
||||
};
|
||||
|
||||
export type SiteEditorState =
|
||||
@@ -14,15 +15,16 @@ type SiteSaveAction =
|
||||
| { kind: 'update'; id: number; payload: SiteForm };
|
||||
|
||||
export function emptySiteForm(): SiteForm {
|
||||
return { name: '', url: '', platform: '', apiKey: '' };
|
||||
return { name: '', url: '', platform: '', apiKey: '', proxyUrl: '' };
|
||||
}
|
||||
|
||||
export function siteFormFromSite(site: Partial<SiteForm>): SiteForm {
|
||||
export function siteFormFromSite(site: Partial<SiteForm> & { proxyUrl?: string | null }): SiteForm {
|
||||
return {
|
||||
name: site.name ?? '',
|
||||
url: site.url ?? '',
|
||||
platform: site.platform ?? '',
|
||||
apiKey: site.apiKey ?? '',
|
||||
proxyUrl: site.proxyUrl ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user