1
0
forked from boos/BoosAPI

fix checkin settings styling and empty updates

This commit is contained in:
cita
2026-03-20 13:31:26 +08:00
parent 029ee10d26
commit 4f94393806
4 changed files with 96 additions and 23 deletions
@@ -340,6 +340,40 @@ describe('checkinService auto relogin', () => {
expect(notifyMock).not.toHaveBeenCalled(); 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 () => { it('treats sub2api checkin unsupported message as skipped', async () => {
selectAllMock.mockReturnValue([ selectAllMock.mockReturnValue([
{ {
+6 -4
View File
@@ -226,10 +226,12 @@ export async function checkinAccount(accountId: number, options?: { skipEvent?:
updates.updatedAt = new Date().toISOString(); updates.updatedAt = new Date().toISOString();
} }
await db.update(schema.accounts) if (Object.keys(updates).length > 0) {
.set(updates) await db.update(schema.accounts)
.where(eq(schema.accounts.id, accountId)) .set(updates)
.run(); .where(eq(schema.accounts.id, accountId))
.run();
}
if (shouldRefreshBalance) { if (shouldRefreshBalance) {
try { try {
+27 -19
View File
@@ -22,6 +22,17 @@ const PROXY_TOKEN_PREFIX = 'sk-';
const ROUTE_BRAND_ICON_PREFIX = 'brand:'; const ROUTE_BRAND_ICON_PREFIX = 'brand:';
const FACTORY_RESET_ADMIN_TOKEN = 'change-me-admin-token'; const FACTORY_RESET_ADMIN_TOKEN = 'change-me-admin-token';
const FACTORY_RESET_CONFIRM_SECONDS = 3; 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 DbDialect = 'sqlite' | 'mysql' | 'postgres';
type RuntimeSettings = { 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 style={{ display: 'grid', gridTemplateColumns: '180px 180px auto', gap: 12, alignItems: 'end', marginBottom: 12 }}>
<div> <div>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 6 }}>签到方式</div> <div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 6 }}>签到方式</div>
<select <ModernSelect
value={runtime.checkinScheduleMode} value={runtime.checkinScheduleMode}
onChange={(e) => setRuntime((prev) => ({ onChange={(value) => setRuntime((prev) => ({
...prev, ...prev,
checkinScheduleMode: e.target.value === 'interval' ? 'interval' : 'cron', checkinScheduleMode: value === 'interval' ? 'interval' : 'cron',
}))} }))}
style={inputStyle} options={CHECKIN_SCHEDULE_MODE_OPTIONS.map((item) => ({ ...item }))}
> />
<option value="cron">Cron</option>
<option value="interval">间隔签到</option>
</select>
</div> </div>
<div> <div>
<div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 6 }}>签到间隔</div> <div style={{ fontSize: 12, color: 'var(--color-text-muted)', marginBottom: 6 }}>签到间隔</div>
<select <ModernSelect
value={String(runtime.checkinIntervalHours)} value={String(runtime.checkinIntervalHours)}
onChange={(e) => setRuntime((prev) => ({ onChange={(value) => setRuntime((prev) => ({
...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'} disabled={runtime.checkinScheduleMode !== 'interval'}
> options={CHECKIN_INTERVAL_OPTIONS}
{Array.from({ length: 24 }, (_, index) => { />
const hour = index + 1;
return <option key={hour} value={hour}>{hour} 小时</option>;
})}
</select>
</div> </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 ? '触发中...' : '测试一次签到'} {testingCheckin ? '触发中...' : '测试一次签到'}
</button> </button>
</div> </div>
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { act, create, type ReactTestInstance } from 'react-test-renderer'; import { act, create, type ReactTestInstance } from 'react-test-renderer';
import { MemoryRouter } from 'react-router-dom'; import { MemoryRouter } from 'react-router-dom';
import { ToastProvider } from '../components/Toast.js'; import { ToastProvider } from '../components/Toast.js';
import ModernSelect from '../components/ModernSelect.js';
import Settings from './Settings.js'; import Settings from './Settings.js';
const { apiMock } = vi.hoisted(() => ({ const { apiMock } = vi.hoisted(() => ({
@@ -145,4 +146,32 @@ describe('Settings log cleanup schedule', () => {
root?.unmount(); 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();
}
});
}); });