diff --git a/blocksuite/affine/data-view/src/__tests__/kanban.unit.spec.ts b/blocksuite/affine/data-view/src/__tests__/kanban.unit.spec.ts index ab12f85c0..c0504f4da 100644 --- a/blocksuite/affine/data-view/src/__tests__/kanban.unit.spec.ts +++ b/blocksuite/affine/data-view/src/__tests__/kanban.unit.spec.ts @@ -9,6 +9,7 @@ import { groupByMatchers } from '../core/group-by/define.js'; import { GroupTrait, sortByManually } from '../core/group-by/trait.js'; import { t } from '../core/logical/type-presets.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 { multiSelectPropertyModelConfig } from '../property-presets/multi-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 { KanbanDragController } from '../view-presets/kanban/pc/controller/drag.js'; import type { KanbanGroup } from '../view-presets/kanban/pc/group.js'; -import type { Row } from '../core/view-manager/row.js'; type Column = { id: string; diff --git a/packages/frontend/core/src/desktop/dialogs/setting/account-setting/integrations-panel.spec.tsx b/packages/frontend/core/src/desktop/dialogs/setting/account-setting/integrations-panel.spec.tsx new file mode 100644 index 000000000..069ad5ae9 --- /dev/null +++ b/packages/frontend/core/src/desktop/dialogs/setting/account-setting/integrations-panel.spec.tsx @@ -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; + disabled?: boolean; + }) => ( + + ), + Input: (props: InputHTMLAttributes) => , + Loading: () =>
loading
, + Menu: ({ children, items }: { children: ReactNode; items: ReactNode }) => ( +
+ {children} +
{items}
+
+ ), + MenuItem: ({ + children, + onSelect, + }: { + children: ReactNode; + onSelect?: () => void; + }) => , + Modal: ({ open, children }: { open: boolean; children: ReactNode }) => + open ?
{children}
: 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) => { + if (key === 'com.affine.integration.calendar.account.count') { + return `${args?.count ?? '0'} calendars`; + } + return key; + }, + } + ), +})); + +vi.mock('@blocksuite/icons/rc', () => ({ + GoogleIcon: () => google-icon, + LinkIcon: () => link-icon, + TodayIcon: () => today-icon, +})); + +vi.mock('@toeverything/infra', async importOriginal => { + const actual = await importOriginal(); + + 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 }) => ( +
{children}
+ ), +})); + +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(); + + 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(); + + 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(); + 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(); + fireEvent.click(screen.getByText('Google Calendar')); + + await waitFor(() => { + expect(gqlMock).toHaveBeenCalledWith({ + query: linkCalendarAccountMutation, + variables: { + input: { + provider: CalendarProviderType.Google, + redirectUri, + }, + }, + }); + }); + }); +});