1
0
forked from boos/BoosAPI

Merge pull request #191 from cita-777/codex/checkin-settings-ui-fix

[codex] fix checkin settings styling and empty updates
This commit is contained in:
Cita
2026-03-20 13:35:17 +08:00
committed by GitHub
4 changed files with 96 additions and 23 deletions
@@ -340,6 +340,40 @@ describe('checkinService auto relogin', () => {
expect(notifyMock).not.toHaveBeenCalled();
});
it('skips account updates when unsupported checkin responses do not change account state', async () => {
selectAllMock.mockReturnValue([
{
accounts: {
id: 18,
username: 'plain-user',
accessToken: 'token',
status: 'active',
extraConfig: null,
},
sites: {
id: 18,
name: 'done-hub',
url: 'https://done.example.com',
platform: 'donehub',
},
},
]);
adapterMock.checkin.mockResolvedValue({
success: false,
message: 'checkin endpoint not found',
});
const { checkinAccount } = await import('./checkinService.js');
const result = await checkinAccount(18);
expect(result.success).toBe(true);
expect(result.status).toBe('skipped');
expect(updateSetMock).not.toHaveBeenCalled();
const firstInsertPayload = insertValuesMock.mock.calls[0]?.[0] as Record<string, unknown>;
expect(firstInsertPayload?.status).toBe('skipped');
});
it('treats sub2api checkin unsupported message as skipped', async () => {
selectAllMock.mockReturnValue([
{
+2
View File
@@ -226,10 +226,12 @@ export async function checkinAccount(accountId: number, options?: { skipEvent?:
updates.updatedAt = new Date().toISOString();
}
if (Object.keys(updates).length > 0) {
await db.update(schema.accounts)
.set(updates)
.where(eq(schema.accounts.id, accountId))
.run();
}
if (shouldRefreshBalance) {
try {
+27 -19
View File
@@ -22,6 +22,17 @@ const PROXY_TOKEN_PREFIX = 'sk-';
const ROUTE_BRAND_ICON_PREFIX = 'brand:';
const FACTORY_RESET_ADMIN_TOKEN = 'change-me-admin-token';
const FACTORY_RESET_CONFIRM_SECONDS = 3;
const CHECKIN_SCHEDULE_MODE_OPTIONS = [
{ value: 'cron', label: 'Cron' },
{ value: 'interval', label: '间隔签到' },
] as const;
const CHECKIN_INTERVAL_OPTIONS = Array.from({ length: 24 }, (_, index) => {
const hour = index + 1;
return {
value: String(hour),
label: `${hour} 小时`,
};
});
type DbDialect = 'sqlite' | 'mysql' | 'postgres';
type RuntimeSettings = {
@@ -979,36 +990,33 @@ export default function Settings() {
<div style={{ display: 'grid', gridTemplateColumns: '180px 180px auto', gap: 12, alignItems: 'end', marginBottom: 12 }}>
<div>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 6 }}>签到方式</div>
<select
<ModernSelect
value={runtime.checkinScheduleMode}
onChange={(e) => setRuntime((prev) => ({
onChange={(value) => setRuntime((prev) => ({
...prev,
checkinScheduleMode: e.target.value === 'interval' ? 'interval' : 'cron',
checkinScheduleMode: value === 'interval' ? 'interval' : 'cron',
}))}
style={inputStyle}
>
<option value="cron">Cron</option>
<option value="interval">间隔签到</option>
</select>
options={CHECKIN_SCHEDULE_MODE_OPTIONS.map((item) => ({ ...item }))}
/>
</div>
<div>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 6 }}>签到间隔</div>
<select
<ModernSelect
value={String(runtime.checkinIntervalHours)}
onChange={(e) => setRuntime((prev) => ({
onChange={(value) => setRuntime((prev) => ({
...prev,
checkinIntervalHours: Math.min(24, Math.max(1, Math.trunc(Number(e.target.value) || 1))),
checkinIntervalHours: Math.min(24, Math.max(1, Math.trunc(Number(value) || 1))),
}))}
style={inputStyle}
disabled={runtime.checkinScheduleMode !== 'interval'}
>
{Array.from({ length: 24 }, (_, index) => {
const hour = index + 1;
return <option key={hour} value={hour}>{hour} 小时</option>;
})}
</select>
options={CHECKIN_INTERVAL_OPTIONS}
/>
</div>
<button onClick={triggerScheduleCheckin} disabled={testingCheckin} className="btn btn-secondary">
<button
onClick={triggerScheduleCheckin}
disabled={testingCheckin}
className="btn btn-ghost"
style={{ border: '1px solid var(--color-border)', whiteSpace: 'nowrap' }}
>
{testingCheckin ? '触发中...' : '测试一次签到'}
</button>
</div>
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { act, create, type ReactTestInstance } from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom';
import { ToastProvider } from '../components/Toast.js';
import ModernSelect from '../components/ModernSelect.js';
import Settings from './Settings.js';
const { apiMock } = vi.hoisted(() => ({
@@ -145,4 +146,32 @@ describe('Settings log cleanup schedule', () => {
root?.unmount();
}
});
it('renders schedule mode controls with modern selects and ghost action styling', async () => {
let root: ReturnType<typeof create> | null = null;
try {
await act(async () => {
root = create(
<MemoryRouter>
<ToastProvider>
<Settings />
</ToastProvider>
</MemoryRouter>,
);
});
await flushMicrotasks();
const triggerButton = root.root.find((node) => (
node.type === 'button'
&& typeof node.props.onClick === 'function'
&& collectText(node).trim() === '测试一次签到'
));
expect(root.root.findAllByType('select')).toHaveLength(0);
expect(root.root.findAllByType(ModernSelect).length).toBeGreaterThanOrEqual(3);
expect(String(triggerButton.props.className || '')).toContain('btn-ghost');
} finally {
root?.unmount();
}
});
});