mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 09:21:24 +08:00
feat(core): add integration panel test
This commit is contained in:
@@ -9,6 +9,7 @@ import { groupByMatchers } from '../core/group-by/define.js';
|
|||||||
import { GroupTrait, sortByManually } from '../core/group-by/trait.js';
|
import { GroupTrait, sortByManually } from '../core/group-by/trait.js';
|
||||||
import { t } from '../core/logical/type-presets.js';
|
import { t } from '../core/logical/type-presets.js';
|
||||||
import type { DataViewCellLifeCycle } from '../core/property/index.js';
|
import type { DataViewCellLifeCycle } from '../core/property/index.js';
|
||||||
|
import type { Row } from '../core/view-manager/row.js';
|
||||||
import { checkboxPropertyModelConfig } from '../property-presets/checkbox/define.js';
|
import { checkboxPropertyModelConfig } from '../property-presets/checkbox/define.js';
|
||||||
import { multiSelectPropertyModelConfig } from '../property-presets/multi-select/define.js';
|
import { multiSelectPropertyModelConfig } from '../property-presets/multi-select/define.js';
|
||||||
import { selectPropertyModelConfig } from '../property-presets/select/define.js';
|
import { selectPropertyModelConfig } from '../property-presets/select/define.js';
|
||||||
@@ -26,7 +27,6 @@ import {
|
|||||||
import type { KanbanCard } from '../view-presets/kanban/pc/card.js';
|
import type { KanbanCard } from '../view-presets/kanban/pc/card.js';
|
||||||
import { KanbanDragController } from '../view-presets/kanban/pc/controller/drag.js';
|
import { KanbanDragController } from '../view-presets/kanban/pc/controller/drag.js';
|
||||||
import type { KanbanGroup } from '../view-presets/kanban/pc/group.js';
|
import type { KanbanGroup } from '../view-presets/kanban/pc/group.js';
|
||||||
import type { Row } from '../core/view-manager/row.js';
|
|
||||||
|
|
||||||
type Column = {
|
type Column = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
+330
@@ -0,0 +1,330 @@
|
|||||||
|
/**
|
||||||
|
* @vitest-environment happy-dom
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
cleanup,
|
||||||
|
fireEvent,
|
||||||
|
render,
|
||||||
|
screen,
|
||||||
|
waitFor,
|
||||||
|
} from '@testing-library/react';
|
||||||
|
import type * as Infra from '@toeverything/infra';
|
||||||
|
import type { InputHTMLAttributes, MouseEventHandler, ReactNode } from 'react';
|
||||||
|
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
const gqlMock = vi.hoisted(() => vi.fn());
|
||||||
|
const openExternal = vi.hoisted(() => vi.fn());
|
||||||
|
const mutateAccounts = vi.hoisted(() => vi.fn(async () => undefined));
|
||||||
|
const mutateProviders = vi.hoisted(() => vi.fn(async () => undefined));
|
||||||
|
const workspaceState = vi.hoisted(() => ({
|
||||||
|
flavour: 'cloud',
|
||||||
|
}));
|
||||||
|
const queryState = vi.hoisted(() => ({
|
||||||
|
accounts: [] as {
|
||||||
|
id: string;
|
||||||
|
provider: string;
|
||||||
|
displayName: string | null;
|
||||||
|
email: string | null;
|
||||||
|
calendarsCount: number;
|
||||||
|
status: string;
|
||||||
|
lastError: string | null;
|
||||||
|
}[],
|
||||||
|
providers: [] as string[],
|
||||||
|
caldavProviders: [] as {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
docsUrl?: string | null;
|
||||||
|
requiresAppPassword?: boolean;
|
||||||
|
}[],
|
||||||
|
}));
|
||||||
|
const GraphQLServiceToken = vi.hoisted(() => class GraphQLService {});
|
||||||
|
const UrlServiceToken = vi.hoisted(() => class UrlService {});
|
||||||
|
const WorkspaceServiceToken = vi.hoisted(() => class WorkspaceService {});
|
||||||
|
|
||||||
|
const CalendarProviderType = vi.hoisted(
|
||||||
|
() =>
|
||||||
|
({
|
||||||
|
Google: 'Google',
|
||||||
|
CalDAV: 'CalDAV',
|
||||||
|
}) as const
|
||||||
|
);
|
||||||
|
|
||||||
|
const calendarAccountsQuery = vi.hoisted(() => Symbol('calendarAccountsQuery'));
|
||||||
|
const calendarProvidersQuery = vi.hoisted(() =>
|
||||||
|
Symbol('calendarProvidersQuery')
|
||||||
|
);
|
||||||
|
const linkCalendarAccountMutation = vi.hoisted(() =>
|
||||||
|
Symbol('linkCalendarAccountMutation')
|
||||||
|
);
|
||||||
|
const unlinkCalendarAccountMutation = vi.hoisted(() =>
|
||||||
|
Symbol('unlinkCalendarAccountMutation')
|
||||||
|
);
|
||||||
|
const linkCalDavAccountMutation = vi.hoisted(() =>
|
||||||
|
Symbol('linkCalDavAccountMutation')
|
||||||
|
);
|
||||||
|
|
||||||
|
vi.mock('@affine/component', () => ({
|
||||||
|
Button: ({
|
||||||
|
children,
|
||||||
|
onClick,
|
||||||
|
disabled,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
disabled?: boolean;
|
||||||
|
}) => (
|
||||||
|
<button disabled={disabled} onClick={onClick}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
),
|
||||||
|
Input: (props: InputHTMLAttributes<HTMLInputElement>) => <input {...props} />,
|
||||||
|
Loading: () => <div>loading</div>,
|
||||||
|
Menu: ({ children, items }: { children: ReactNode; items: ReactNode }) => (
|
||||||
|
<div>
|
||||||
|
{children}
|
||||||
|
<div>{items}</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
MenuItem: ({
|
||||||
|
children,
|
||||||
|
onSelect,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
onSelect?: () => void;
|
||||||
|
}) => <button onClick={onSelect}>{children}</button>,
|
||||||
|
Modal: ({ open, children }: { open: boolean; children: ReactNode }) =>
|
||||||
|
open ? <div>{children}</div> : null,
|
||||||
|
notify: {
|
||||||
|
error: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@affine/core/components/hooks/use-query', () => ({
|
||||||
|
useQuery: ({ query }: { query: symbol }) => {
|
||||||
|
if (query === calendarAccountsQuery) {
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
currentUser: {
|
||||||
|
calendarAccounts: queryState.accounts,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
mutate: mutateAccounts,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
serverConfig: {
|
||||||
|
calendarProviders: queryState.providers,
|
||||||
|
calendarCalDAVProviders: queryState.caldavProviders,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
isLoading: false,
|
||||||
|
mutate: mutateProviders,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@affine/core/modules/cloud', () => ({
|
||||||
|
GraphQLService: GraphQLServiceToken,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@affine/core/modules/url', () => ({
|
||||||
|
UrlService: UrlServiceToken,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@affine/core/modules/workspace', () => ({
|
||||||
|
WorkspaceService: WorkspaceServiceToken,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@affine/graphql', () => ({
|
||||||
|
calendarAccountsQuery,
|
||||||
|
calendarProvidersQuery,
|
||||||
|
CalendarProviderType,
|
||||||
|
linkCalendarAccountMutation,
|
||||||
|
unlinkCalendarAccountMutation,
|
||||||
|
linkCalDavAccountMutation,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@affine/i18n', () => ({
|
||||||
|
useI18n: () =>
|
||||||
|
new Proxy(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
get: (_, key: string) => (args?: Record<string, string>) => {
|
||||||
|
if (key === 'com.affine.integration.calendar.account.count') {
|
||||||
|
return `${args?.count ?? '0'} calendars`;
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@blocksuite/icons/rc', () => ({
|
||||||
|
GoogleIcon: () => <span>google-icon</span>,
|
||||||
|
LinkIcon: () => <span>link-icon</span>,
|
||||||
|
TodayIcon: () => <span>today-icon</span>,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('@toeverything/infra', async importOriginal => {
|
||||||
|
const actual = await importOriginal<typeof Infra>();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useService: (token: unknown) => {
|
||||||
|
if (token === GraphQLServiceToken) {
|
||||||
|
return {
|
||||||
|
gql: gqlMock,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token === UrlServiceToken) {
|
||||||
|
return {
|
||||||
|
openExternal,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token === WorkspaceServiceToken) {
|
||||||
|
return {
|
||||||
|
workspace: {
|
||||||
|
flavour: workspaceState.flavour,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock('../layout', () => ({
|
||||||
|
CollapsibleWrapper: ({ children }: { children: ReactNode }) => (
|
||||||
|
<div>{children}</div>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { IntegrationsPanel } from './integrations-panel';
|
||||||
|
|
||||||
|
describe('IntegrationsPanel', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cleanup();
|
||||||
|
gqlMock.mockReset();
|
||||||
|
openExternal.mockReset();
|
||||||
|
mutateAccounts.mockClear();
|
||||||
|
mutateProviders.mockClear();
|
||||||
|
workspaceState.flavour = 'cloud';
|
||||||
|
queryState.accounts = [];
|
||||||
|
queryState.providers = [];
|
||||||
|
queryState.caldavProviders = [];
|
||||||
|
window.history.replaceState({}, '', '/workspace/workspace-1/all');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('opens calendar workspace settings from an account row in cloud workspaces', () => {
|
||||||
|
queryState.accounts = [
|
||||||
|
{
|
||||||
|
id: 'account-1',
|
||||||
|
provider: CalendarProviderType.Google,
|
||||||
|
displayName: 'Alice',
|
||||||
|
email: 'alice@example.com',
|
||||||
|
calendarsCount: 2,
|
||||||
|
status: 'active',
|
||||||
|
lastError: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const onChangeSettingState = vi.fn();
|
||||||
|
|
||||||
|
render(<IntegrationsPanel onChangeSettingState={onChangeSettingState} />);
|
||||||
|
|
||||||
|
const row = screen.getByRole('button', { name: /Alice/i });
|
||||||
|
fireEvent.click(row);
|
||||||
|
fireEvent.keyDown(row, { key: 'Enter' });
|
||||||
|
|
||||||
|
expect(onChangeSettingState).toHaveBeenNthCalledWith(1, {
|
||||||
|
activeTab: 'workspace:integrations',
|
||||||
|
scrollAnchor: 'integration-calendar',
|
||||||
|
});
|
||||||
|
expect(onChangeSettingState).toHaveBeenNthCalledWith(2, {
|
||||||
|
activeTab: 'workspace:integrations',
|
||||||
|
scrollAnchor: 'integration-calendar',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not expose account rows as buttons in local workspaces', () => {
|
||||||
|
workspaceState.flavour = 'local';
|
||||||
|
queryState.accounts = [
|
||||||
|
{
|
||||||
|
id: 'account-1',
|
||||||
|
provider: CalendarProviderType.Google,
|
||||||
|
displayName: 'Alice',
|
||||||
|
email: 'alice@example.com',
|
||||||
|
calendarsCount: 2,
|
||||||
|
status: 'active',
|
||||||
|
lastError: null,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const onChangeSettingState = vi.fn();
|
||||||
|
|
||||||
|
render(<IntegrationsPanel onChangeSettingState={onChangeSettingState} />);
|
||||||
|
|
||||||
|
expect(screen.queryByRole('button', { name: /Alice/i })).toBeNull();
|
||||||
|
fireEvent.click(screen.getByText('Alice'));
|
||||||
|
|
||||||
|
expect(onChangeSettingState).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps the current page as oauth redirect in local workspaces', async () => {
|
||||||
|
workspaceState.flavour = 'local';
|
||||||
|
queryState.providers = [CalendarProviderType.Google];
|
||||||
|
gqlMock.mockResolvedValue({
|
||||||
|
linkCalendarAccount: 'https://calendar.example.com/oauth',
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<IntegrationsPanel />);
|
||||||
|
fireEvent.click(screen.getByText('Google Calendar'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(gqlMock).toHaveBeenCalledWith({
|
||||||
|
query: linkCalendarAccountMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
provider: CalendarProviderType.Google,
|
||||||
|
redirectUri: window.location.href,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('redirects oauth back to workspace integrations in cloud workspaces', async () => {
|
||||||
|
queryState.providers = [CalendarProviderType.Google];
|
||||||
|
gqlMock.mockResolvedValue({
|
||||||
|
linkCalendarAccount: 'https://calendar.example.com/oauth',
|
||||||
|
});
|
||||||
|
const redirectUri = new URL(
|
||||||
|
'/workspace/workspace-1/settings?tab=workspace%3Aintegrations&scrollAnchor=integration-calendar',
|
||||||
|
window.location.origin
|
||||||
|
).toString();
|
||||||
|
|
||||||
|
render(<IntegrationsPanel />);
|
||||||
|
fireEvent.click(screen.getByText('Google Calendar'));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(gqlMock).toHaveBeenCalledWith({
|
||||||
|
query: linkCalendarAccountMutation,
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
provider: CalendarProviderType.Google,
|
||||||
|
redirectUri,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user