Makelore 2.0 initial clean snapshot
This commit is contained in:
361
tests/unit/login-page.test.tsx
Normal file
361
tests/unit/login-page.test.tsx
Normal file
@@ -0,0 +1,361 @@
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { Login } from '@/pages/Login';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import App from '@/App';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { useOpencodeStore } from '@/stores/opencode';
|
||||
import { useProjectConfigStore } from '@/stores/project-config';
|
||||
import { createProjectConfig } from '../../shared/project-config';
|
||||
|
||||
const hostApiFetchMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('@/lib/host-api', () => ({
|
||||
hostApiFetch: (...args: unknown[]) => hostApiFetchMock(...args),
|
||||
}));
|
||||
|
||||
function resetAuthStore() {
|
||||
useAuthStore.setState({
|
||||
initialized: true,
|
||||
loading: false,
|
||||
error: null,
|
||||
authBase: '',
|
||||
clientId: 'app',
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
tokenType: null,
|
||||
expiresAt: null,
|
||||
user: null,
|
||||
});
|
||||
}
|
||||
|
||||
describe('Login page', () => {
|
||||
beforeEach(() => {
|
||||
window.electron.imageWorkspaceLocalDevelopment = false;
|
||||
window.localStorage.clear();
|
||||
hostApiFetchMock.mockReset();
|
||||
resetAuthStore();
|
||||
useSettingsStore.getState().resetSettings();
|
||||
useOpencodeStore.setState({ projects: [], activeProject: null });
|
||||
useProjectConfigStore.setState({ configsByProjectId: {}, knowledgeByProjectId: {}, loadingProjectId: null, errorsByProjectId: {} });
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('keeps the login route reachable from the app router', () => {
|
||||
vi.stubGlobal('__NIANCODE_RENDERER_ONLY__', true);
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/login']}>
|
||||
<App />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: 'Continue in browser' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('redirects protected app routes to login when setup is complete but the user is signed out', async () => {
|
||||
useSettingsStore.setState({ setupComplete: true });
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/models']}>
|
||||
<App />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('button', { name: 'Continue in browser' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens only the local image workspace anonymously when the explicit development mode is active', async () => {
|
||||
useSettingsStore.setState({ setupComplete: true });
|
||||
window.electron.imageWorkspaceLocalDevelopment = true;
|
||||
hostApiFetchMock.mockImplementation(async (path: string) => {
|
||||
if (path === '/api/works/image-workspace') {
|
||||
return {
|
||||
success: true,
|
||||
workspace: {
|
||||
activeProjectId: null,
|
||||
capabilities: {
|
||||
modes: [],
|
||||
models: [],
|
||||
aspectRatios: [],
|
||||
resolutions: [],
|
||||
outputCounts: [],
|
||||
maxReferenceImages: 0,
|
||||
referenceUpload: { enabled: false, acceptedMimeTypes: [] },
|
||||
},
|
||||
projects: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
const { unmount } = render(
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<App />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('创建第一个项目')).toBeInTheDocument();
|
||||
expect(screen.queryByText('本地开发')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'Continue in browser' })).not.toBeInTheDocument();
|
||||
unmount();
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/models']}>
|
||||
<App />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
expect(await screen.findByRole('button', { name: 'Continue in browser' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the project conversation from the default route for an initialized active project', async () => {
|
||||
vi.stubGlobal('__NIANCODE_RENDERER_ONLY__', true);
|
||||
const project = {
|
||||
id: 'project-default-chat',
|
||||
path: '/tmp/project-default-chat',
|
||||
name: 'default-chat',
|
||||
createdAt: '2026-07-12T00:00:00.000Z',
|
||||
updatedAt: '2026-07-12T00:00:00.000Z',
|
||||
lastOpenedAt: '2026-07-12T00:00:00.000Z',
|
||||
};
|
||||
const config = { ...createProjectConfig('standard-development'), initialized: true };
|
||||
useOpencodeStore.setState({ projects: [project], activeProject: project });
|
||||
hostApiFetchMock.mockImplementation(async (path: string) => {
|
||||
if (path === '/api/opencode/projects') return { projects: [project], activeProject: project };
|
||||
if (path.startsWith('/api/opencode/projects/config?')) return { status: 'valid', config, knowledgeFiles: [] };
|
||||
if (path === '/api/opencode/status') return { state: 'stopped', port: 4096 };
|
||||
if (path === '/api/opencode/config-summary') return { providerIds: [], providerCount: 0, envKeys: [] };
|
||||
if (path === '/api/provider-accounts') return [];
|
||||
if (path === '/api/provider-accounts/key-info') return [];
|
||||
if (path === '/api/provider-vendors') return [];
|
||||
if (path === '/api/provider-accounts/default') return { accountId: null };
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<App />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId('chat-operation-page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps the default route on project configuration without an active project', async () => {
|
||||
vi.stubGlobal('__NIANCODE_RENDERER_ONLY__', true);
|
||||
hostApiFetchMock.mockImplementation(async (path: string) => {
|
||||
if (path === '/api/opencode/projects') return { projects: [], activeProject: null };
|
||||
if (path === '/api/opencode/status') return { state: 'stopped', port: 4096 };
|
||||
if (path === '/api/opencode/config-summary') return { providerIds: [], providerCount: 0, envKeys: [] };
|
||||
if (path === '/api/provider-accounts') return [];
|
||||
if (path === '/api/provider-accounts/key-info') return [];
|
||||
if (path === '/api/provider-vendors') return [];
|
||||
if (path === '/api/provider-accounts/default') return { accountId: null };
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<App />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('请先新建项目')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('chat-operation-page')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('keeps an uninitialized active project on project configuration', async () => {
|
||||
vi.stubGlobal('__NIANCODE_RENDERER_ONLY__', true);
|
||||
const project = {
|
||||
id: 'project-uninitialized',
|
||||
path: '/tmp/project-uninitialized',
|
||||
name: 'uninitialized',
|
||||
createdAt: '2026-07-12T00:00:00.000Z',
|
||||
updatedAt: '2026-07-12T00:00:00.000Z',
|
||||
lastOpenedAt: '2026-07-12T00:00:00.000Z',
|
||||
};
|
||||
const config = { ...createProjectConfig('standard-development'), initialized: false };
|
||||
useOpencodeStore.setState({ projects: [project], activeProject: project });
|
||||
hostApiFetchMock.mockImplementation(async (path: string) => {
|
||||
if (path === '/api/opencode/projects') return { projects: [project], activeProject: project };
|
||||
if (path.startsWith('/api/opencode/projects/config?')) return { status: 'valid', config, knowledgeFiles: [] };
|
||||
if (path === '/api/opencode/status') return { state: 'stopped', port: 4096 };
|
||||
if (path === '/api/opencode/config-summary') return { providerIds: [], providerCount: 0, envKeys: [] };
|
||||
if (path === '/api/provider-accounts') return [];
|
||||
if (path === '/api/provider-accounts/key-info') return [];
|
||||
if (path === '/api/provider-vendors') return [];
|
||||
if (path === '/api/provider-accounts/default') return { accountId: null };
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/']}>
|
||||
<App />
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('在这里配置你的项目基础!')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('chat-operation-page')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('starts browser authorization and enters the app after success', async () => {
|
||||
hostApiFetchMock.mockImplementation(async (path: string) => {
|
||||
if (path === '/api/auth/browser/start') {
|
||||
return {
|
||||
success: true,
|
||||
token: {
|
||||
access_token: 'access-token',
|
||||
refresh_token: 'refresh-token',
|
||||
token_type: 'Bearer',
|
||||
expires_in: 43200,
|
||||
username: 'zhangsan',
|
||||
user_id: '1',
|
||||
},
|
||||
};
|
||||
}
|
||||
if (path === '/api/auth/session/sync') return { success: true };
|
||||
if (path === '/api/provider-accounts/import-user-model-config') {
|
||||
return {
|
||||
success: true,
|
||||
account: {
|
||||
id: 'niancode-user-models',
|
||||
vendorId: 'custom',
|
||||
label: 'Makelore Models',
|
||||
authMode: 'api_key',
|
||||
model: 'gpt-4.1-mini',
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
createdAt: '2026-07-07T00:00:00.000Z',
|
||||
updatedAt: '2026-07-07T00:00:00.000Z',
|
||||
},
|
||||
importedModels: ['gpt-4.1-mini'],
|
||||
};
|
||||
}
|
||||
if (path === '/api/provider-accounts') return [];
|
||||
if (path === '/api/provider-accounts/key-info') return [];
|
||||
if (path === '/api/provider-vendors') return [];
|
||||
if (path === '/api/provider-accounts/default') return { accountId: null };
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/login']}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/module-select" element={<div>Module Selection</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
expect(screen.queryByLabelText('Username')).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Password')).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Captcha')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Continue in browser' }));
|
||||
|
||||
await screen.findByText('Module Selection');
|
||||
await waitFor(() => {
|
||||
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/auth/browser/start', {
|
||||
method: 'POST',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('syncs the current Works Square user model config after browser login', async () => {
|
||||
hostApiFetchMock.mockImplementation(async (path: string) => {
|
||||
if (path === '/api/auth/browser/start') {
|
||||
return {
|
||||
success: true,
|
||||
token: {
|
||||
access_token: 'fresh-access-token',
|
||||
refresh_token: 'refresh-token',
|
||||
token_type: 'Bearer',
|
||||
expires_in: 43200,
|
||||
username: 'zhangsan',
|
||||
user_id: '1',
|
||||
},
|
||||
};
|
||||
}
|
||||
if (path === '/api/auth/session/sync') {
|
||||
return { success: true };
|
||||
}
|
||||
if (path === '/api/provider-accounts/import-user-model-config') {
|
||||
return {
|
||||
success: true,
|
||||
account: {
|
||||
id: 'niancode-user-models',
|
||||
vendorId: 'custom',
|
||||
label: 'Makelore Models',
|
||||
authMode: 'api_key',
|
||||
model: 'gpt-4.1-mini',
|
||||
fallbackModels: ['claude-3-5-haiku'],
|
||||
enabled: true,
|
||||
isDefault: true,
|
||||
createdAt: '2026-07-07T00:00:00.000Z',
|
||||
updatedAt: '2026-07-07T00:00:00.000Z',
|
||||
},
|
||||
importedModels: ['gpt-4.1-mini', 'claude-3-5-haiku'],
|
||||
};
|
||||
}
|
||||
if (path === '/api/provider-accounts/default') {
|
||||
return { accountId: 'niancode-user-models' };
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/login']}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/module-select" element={<div>Module Selection</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Continue in browser' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/provider-accounts/import-user-model-config', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ accessToken: 'fresh-access-token' }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the user on login when model config sync fails after browser login', async () => {
|
||||
hostApiFetchMock
|
||||
.mockResolvedValueOnce({
|
||||
success: true,
|
||||
token: {
|
||||
access_token: 'fresh-access-token',
|
||||
refresh_token: 'refresh-token',
|
||||
token_type: 'Bearer',
|
||||
expires_in: 43200,
|
||||
username: 'zhangsan',
|
||||
user_id: '1',
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({ success: true })
|
||||
.mockRejectedValueOnce(new Error('sync failed'))
|
||||
.mockResolvedValueOnce({ success: true });
|
||||
|
||||
render(
|
||||
<MemoryRouter initialEntries={['/login']}>
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/module-select" element={<div>Module Selection</div>} />
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Continue in browser' }));
|
||||
|
||||
expect(await screen.findByText('sync failed')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Module Selection')).not.toBeInTheDocument();
|
||||
expect(hostApiFetchMock).toHaveBeenCalledWith('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ accessToken: 'fresh-access-token' }),
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user