mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-09-17 07:17:18 +00:00
fix(bots): warn about incomplete processor event support
This commit is contained in:
@@ -782,6 +782,7 @@ const BotForm = forwardRef<BotFormHandle, BotFormProps>(function BotForm(
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<PluginProcessorBindings
|
<PluginProcessorBindings
|
||||||
|
supportedEvents={adapterSupportedEvents[currentAdapter] || []}
|
||||||
value={form.watch('plugin_processors') ?? []}
|
value={form.watch('plugin_processors') ?? []}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
form.setValue('plugin_processors', value, {
|
form.setValue('plugin_processors', value, {
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ import { backendClient } from '@/app/infra/http';
|
|||||||
import {
|
import {
|
||||||
eventGroupLabel,
|
eventGroupLabel,
|
||||||
eventNamespaces,
|
eventNamespaces,
|
||||||
|
eventPatternCovers,
|
||||||
groupEventPatterns,
|
groupEventPatterns,
|
||||||
} from '@/app/home/components/event-patterns/event-pattern-groups';
|
} from '@/app/home/components/event-patterns/event-pattern-groups';
|
||||||
import EventSelectOptionContent from '@/app/home/components/event-patterns/EventSelectOptionContent';
|
import EventSelectOptionContent from '@/app/home/components/event-patterns/EventSelectOptionContent';
|
||||||
@@ -220,17 +221,6 @@ function isMessageEventPattern(p: string) {
|
|||||||
return p === 'message.*' || p.startsWith('message.');
|
return p === 'message.*' || p.startsWith('message.');
|
||||||
}
|
}
|
||||||
|
|
||||||
function eventPatternCovers(sup: string, bind: string) {
|
|
||||||
if (sup === '*') return true;
|
|
||||||
if (sup === bind) return true;
|
|
||||||
if (bind === '*') return false;
|
|
||||||
if (sup.endsWith('.*')) {
|
|
||||||
const ns = sup.replace('.*', '');
|
|
||||||
return bind === `${ns}.*` || bind.startsWith(`${ns}.`);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RouteConflict {
|
interface RouteConflict {
|
||||||
winnerIndex: number;
|
winnerIndex: number;
|
||||||
shadowedIndex: number;
|
shadowedIndex: number;
|
||||||
@@ -1753,45 +1743,46 @@ export default function EventBindingsEditor({
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<DropdownMenuSeparator />
|
{otherEventGroups.length > 0 && (
|
||||||
<DropdownMenuSub>
|
<>
|
||||||
<DropdownMenuSubTrigger
|
<DropdownMenuSeparator />
|
||||||
className="items-start gap-2 py-2"
|
<DropdownMenuSub>
|
||||||
disabled={otherEventGroups.length === 0}
|
<DropdownMenuSubTrigger className="items-start gap-2 py-2">
|
||||||
>
|
<Workflow className="mt-0.5 h-4 w-4 shrink-0" />
|
||||||
<Workflow className="mt-0.5 h-4 w-4 shrink-0" />
|
<span className="flex min-w-0 flex-col gap-0.5 pr-2">
|
||||||
<span className="flex min-w-0 flex-col gap-0.5 pr-2">
|
<span>{t('bots.behaviorCustom')}</span>
|
||||||
<span>{t('bots.behaviorCustom')}</span>
|
<span className="text-xs font-normal text-muted-foreground">
|
||||||
<span className="text-xs font-normal text-muted-foreground">
|
{t('bots.behaviorCustomDescription')}
|
||||||
{t('bots.behaviorCustomDescription')}
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</DropdownMenuSubTrigger>
|
||||||
</DropdownMenuSubTrigger>
|
<DropdownMenuSubContent className="max-h-[min(70vh,32rem)] w-[320px] max-w-[90vw] overflow-y-auto">
|
||||||
<DropdownMenuSubContent className="max-h-[min(70vh,32rem)] w-[320px] max-w-[90vw] overflow-y-auto">
|
{otherEventGroups.map((group, groupIndex) => (
|
||||||
{otherEventGroups.map((group, groupIndex) => (
|
<Fragment key={group.namespace}>
|
||||||
<Fragment key={group.namespace}>
|
{groupIndex > 0 && <DropdownMenuSeparator />}
|
||||||
{groupIndex > 0 && <DropdownMenuSeparator />}
|
<DropdownMenuLabel className="px-2 py-1 text-xs font-normal text-muted-foreground">
|
||||||
<DropdownMenuLabel className="px-2 py-1 text-xs font-normal text-muted-foreground">
|
{eventGroupLabel(group.namespace, t)}
|
||||||
{eventGroupLabel(group.namespace, t)}
|
</DropdownMenuLabel>
|
||||||
</DropdownMenuLabel>
|
{group.patterns.map((event) => (
|
||||||
{group.patterns.map((event) => (
|
<DropdownMenuItem
|
||||||
<DropdownMenuItem
|
key={event}
|
||||||
key={event}
|
className="items-start py-2"
|
||||||
className="items-start py-2"
|
onClick={() => addBinding(event)}
|
||||||
onClick={() => addBinding(event)}
|
>
|
||||||
>
|
<span className="flex min-w-0 flex-col gap-0.5">
|
||||||
<span className="flex min-w-0 flex-col gap-0.5">
|
<span>{eventLabel(event, t)}</span>
|
||||||
<span>{eventLabel(event, t)}</span>
|
<span className="text-xs text-muted-foreground">
|
||||||
<span className="text-xs text-muted-foreground">
|
{eventDescription(event, t)}
|
||||||
{eventDescription(event, t)}
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
))}
|
||||||
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</Fragment>
|
</DropdownMenuSubContent>
|
||||||
))}
|
</DropdownMenuSub>
|
||||||
</DropdownMenuSubContent>
|
</>
|
||||||
</DropdownMenuSub>
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<RouteDryRunDialog
|
<RouteDryRunDialog
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Check, Plus, Settings2, ScrollText, X } from 'lucide-react';
|
import {
|
||||||
|
Check,
|
||||||
|
Plus,
|
||||||
|
Settings2,
|
||||||
|
ScrollText,
|
||||||
|
TriangleAlert,
|
||||||
|
X,
|
||||||
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
||||||
import type {
|
import type {
|
||||||
@@ -11,7 +18,10 @@ import type {
|
|||||||
} from '@/app/infra/entities/api';
|
} from '@/app/infra/entities/api';
|
||||||
import { httpClient } from '@/app/infra/http';
|
import { httpClient } from '@/app/infra/http';
|
||||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||||
import { eventPatternLabel } from '@/app/home/components/event-patterns/event-pattern-groups';
|
import {
|
||||||
|
eventPatternCovers,
|
||||||
|
eventPatternLabel,
|
||||||
|
} from '@/app/home/components/event-patterns/event-pattern-groups';
|
||||||
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
import DynamicFormComponent from '@/app/home/components/dynamic-form/DynamicFormComponent';
|
||||||
import { AuthenticatedPluginIcon } from '@/components/AuthenticatedPluginIcon';
|
import { AuthenticatedPluginIcon } from '@/components/AuthenticatedPluginIcon';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
@@ -35,16 +45,52 @@ import {
|
|||||||
} from '@/components/ui/dialog';
|
} from '@/components/ui/dialog';
|
||||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||||
|
|
||||||
|
function EventCompatibilityWarning({
|
||||||
|
patterns = [],
|
||||||
|
supportedEvents,
|
||||||
|
}: {
|
||||||
|
patterns?: string[];
|
||||||
|
supportedEvents: string[];
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
// Legacy adapters only emit message.received when no events are declared.
|
||||||
|
const supported = supportedEvents.length
|
||||||
|
? supportedEvents
|
||||||
|
: ['message.received'];
|
||||||
|
const unsupported = [...new Set(patterns)].filter(
|
||||||
|
(pattern) => !supported.some((event) => eventPatternCovers(event, pattern)),
|
||||||
|
);
|
||||||
|
if (!unsupported.length) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
role="status"
|
||||||
|
className="mt-2 flex items-start gap-1.5 whitespace-normal text-xs text-amber-700 dark:text-amber-400"
|
||||||
|
>
|
||||||
|
<TriangleAlert className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
|
||||||
|
<span>
|
||||||
|
{t('bots.pluginSubscriptions.incompleteEvents', {
|
||||||
|
events: unsupported
|
||||||
|
.map((pattern) => eventPatternLabel(pattern, t))
|
||||||
|
.join(' · '),
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function PluginProcessorBindings({
|
export default function PluginProcessorBindings({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
agents,
|
agents,
|
||||||
onCreated,
|
onCreated,
|
||||||
|
supportedEvents,
|
||||||
}: {
|
}: {
|
||||||
value: PluginProcessorBinding[];
|
value: PluginProcessorBinding[];
|
||||||
onChange: (value: PluginProcessorBinding[]) => void;
|
onChange: (value: PluginProcessorBinding[]) => void;
|
||||||
agents: Agent[];
|
agents: Agent[];
|
||||||
onCreated: (agent: Agent) => void;
|
onCreated: (agent: Agent) => void;
|
||||||
|
supportedEvents: string[];
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { refreshPipelines } = useSidebarData();
|
const { refreshPipelines } = useSidebarData();
|
||||||
@@ -189,6 +235,10 @@ export default function PluginProcessorBindings({
|
|||||||
.map((pattern) => eventPatternLabel(pattern, t))
|
.map((pattern) => eventPatternLabel(pattern, t))
|
||||||
.join(' · ') || t('agents.eventProcessor.unavailable')}
|
.join(' · ') || t('agents.eventProcessor.unavailable')}
|
||||||
</p>
|
</p>
|
||||||
|
<EventCompatibilityWarning
|
||||||
|
patterns={agent?.supported_event_patterns}
|
||||||
|
supportedEvents={supportedEvents}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex shrink-0 items-center gap-1">
|
<div className="flex shrink-0 items-center gap-1">
|
||||||
{agent &&
|
{agent &&
|
||||||
@@ -329,6 +379,10 @@ export default function PluginProcessorBindings({
|
|||||||
.map((pattern) => eventPatternLabel(pattern, t))
|
.map((pattern) => eventPatternLabel(pattern, t))
|
||||||
.join(' · ')}
|
.join(' · ')}
|
||||||
</span>
|
</span>
|
||||||
|
<EventCompatibilityWarning
|
||||||
|
patterns={agent.supported_event_patterns}
|
||||||
|
supportedEvents={supportedEvents}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
@@ -392,6 +446,10 @@ export default function PluginProcessorBindings({
|
|||||||
.map((pattern) => eventPatternLabel(pattern, t))
|
.map((pattern) => eventPatternLabel(pattern, t))
|
||||||
.join(' · ')}
|
.join(' · ')}
|
||||||
</span>
|
</span>
|
||||||
|
<EventCompatibilityWarning
|
||||||
|
patterns={descriptor.supported_event_patterns}
|
||||||
|
supportedEvents={supportedEvents}
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
{componentRef === descriptor.id && (
|
{componentRef === descriptor.id && (
|
||||||
<Check className="size-4 shrink-0" />
|
<Check className="size-4 shrink-0" />
|
||||||
|
|||||||
@@ -5,6 +5,13 @@ export interface EventPatternGroup {
|
|||||||
patterns: string[];
|
patterns: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function eventPatternCovers(supported: string, required: string) {
|
||||||
|
if (supported === '*' || supported === required) return true;
|
||||||
|
return (
|
||||||
|
supported.endsWith('.*') && required.startsWith(supported.slice(0, -1))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function eventPatternNamespace(pattern: string) {
|
function eventPatternNamespace(pattern: string) {
|
||||||
if (pattern === '*') return '*';
|
if (pattern === '*') return '*';
|
||||||
return pattern.split('.')[0] || pattern;
|
return pattern.split('.')[0] || pattern;
|
||||||
|
|||||||
@@ -356,6 +356,8 @@ const enUS = {
|
|||||||
},
|
},
|
||||||
bots: {
|
bots: {
|
||||||
pluginSubscriptions: {
|
pluginSubscriptions: {
|
||||||
|
incompleteEvents:
|
||||||
|
'This bot cannot fully cover these subscribed events: {{events}}. The processor may work only partially; you can still select and bind it.',
|
||||||
description:
|
description:
|
||||||
'Automatically receive events declared by the plugin, independently of the routes above.',
|
'Automatically receive events declared by the plugin, independently of the routes above.',
|
||||||
empty: 'No plugin processors are bound.',
|
empty: 'No plugin processors are bound.',
|
||||||
|
|||||||
@@ -362,6 +362,8 @@ const jaJP = {
|
|||||||
},
|
},
|
||||||
bots: {
|
bots: {
|
||||||
pluginSubscriptions: {
|
pluginSubscriptions: {
|
||||||
|
incompleteEvents:
|
||||||
|
'このボットは次の監視イベントに完全には対応していません:{{events}}。一部の機能が動作しない可能性がありますが、選択して登録できます。',
|
||||||
description:
|
description:
|
||||||
'プラグインが宣言したイベントを自動で受信し、上のルートとは独立して実行します。',
|
'プラグインが宣言したイベントを自動で受信し、上のルートとは独立して実行します。',
|
||||||
empty: 'プラグインプロセッサーは未登録です。',
|
empty: 'プラグインプロセッサーは未登録です。',
|
||||||
|
|||||||
@@ -341,6 +341,8 @@ const zhHans = {
|
|||||||
},
|
},
|
||||||
bots: {
|
bots: {
|
||||||
pluginSubscriptions: {
|
pluginSubscriptions: {
|
||||||
|
incompleteEvents:
|
||||||
|
'当前机器人无法完整支持以下监听事件:{{events}}。处理器的功能可能不完整,仍可选择并绑定。',
|
||||||
description: '自动接收插件声明的事件,与上方事件路由独立执行。',
|
description: '自动接收插件声明的事件,与上方事件路由独立执行。',
|
||||||
empty: '尚未绑定插件处理器。',
|
empty: '尚未绑定插件处理器。',
|
||||||
add: '添加插件处理器',
|
add: '添加插件处理器',
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
import { expect, test, type Page } from '@playwright/test';
|
||||||
|
import { installLangBotApiMocks } from './fixtures/langbot-api';
|
||||||
|
|
||||||
|
const warning = /This bot cannot fully cover these subscribed events:/;
|
||||||
|
const definitions = [
|
||||||
|
{ name: 'Messages only', patterns: ['message.received'] },
|
||||||
|
{
|
||||||
|
name: 'Community helper',
|
||||||
|
patterns: [
|
||||||
|
'message.received',
|
||||||
|
'group.member_joined',
|
||||||
|
'friend.request_received',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: 'Group observer', patterns: ['group.*'] },
|
||||||
|
{ name: 'All observer', patterns: ['*'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function setup(page: Page, events?: string[]) {
|
||||||
|
await installLangBotApiMocks(page, { authenticated: true });
|
||||||
|
const components = definitions.map(({ name, patterns }, index) => ({
|
||||||
|
id: `plugin:qa/events/component-${index}`,
|
||||||
|
label: { en_US: name },
|
||||||
|
supported_event_patterns: patterns,
|
||||||
|
config_schema: [],
|
||||||
|
plugin_author: 'qa',
|
||||||
|
plugin_name: 'events',
|
||||||
|
}));
|
||||||
|
const agents = components.map((component, index) => ({
|
||||||
|
uuid: `processor-${index}`,
|
||||||
|
name: definitions[index].name,
|
||||||
|
kind: 'event_processor',
|
||||||
|
component_ref: component.id,
|
||||||
|
supported_event_patterns: component.supported_event_patterns,
|
||||||
|
config: {},
|
||||||
|
}));
|
||||||
|
await page.route('**/api/v1/agents**', async (route) => {
|
||||||
|
const path = new URL(route.request().url()).pathname;
|
||||||
|
let data: unknown;
|
||||||
|
if (path.endsWith('/_/metadata')) data = { event_processors: components };
|
||||||
|
else if (path === '/api/v1/agents' && route.request().method() === 'POST') {
|
||||||
|
const payload = route.request().postDataJSON();
|
||||||
|
agents.push({
|
||||||
|
...payload,
|
||||||
|
uuid: 'processor-new',
|
||||||
|
supported_event_patterns: components.find(
|
||||||
|
(item) => item.id === payload.component_ref,
|
||||||
|
)!.supported_event_patterns,
|
||||||
|
});
|
||||||
|
data = { uuid: 'processor-new', kind: 'event_processor' };
|
||||||
|
} else if (path === '/api/v1/agents') data = { agents };
|
||||||
|
else return route.fallback();
|
||||||
|
await route.fulfill({ json: { code: 0, data } });
|
||||||
|
});
|
||||||
|
await page.route('**/api/v1/platform/adapters', (route) =>
|
||||||
|
route.fulfill({
|
||||||
|
json: {
|
||||||
|
code: 0,
|
||||||
|
data: {
|
||||||
|
adapters: [
|
||||||
|
{
|
||||||
|
name: 'playwright-adapter',
|
||||||
|
label: { en_US: 'Message adapter' },
|
||||||
|
description: { en_US: 'Messages' },
|
||||||
|
spec: {
|
||||||
|
config: [],
|
||||||
|
...(events ? { supported_events: events } : {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'full-adapter',
|
||||||
|
label: { en_US: 'Community adapter' },
|
||||||
|
description: { en_US: 'Community events' },
|
||||||
|
spec: {
|
||||||
|
config: [],
|
||||||
|
supported_events: [
|
||||||
|
'message.received',
|
||||||
|
'group.member_joined',
|
||||||
|
'friend.request_received',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await page.goto('/home/bots?id=compatibility-bot');
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Add plugin processor', exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
test('partial support warns in existing choices and saved bindings, and changes with adapter', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await setup(page, ['message.received']);
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'Add plugin processor', exact: true })
|
||||||
|
.click();
|
||||||
|
const dialog = page.getByRole('dialog');
|
||||||
|
const community = dialog
|
||||||
|
.locator('label')
|
||||||
|
.filter({ hasText: 'Community helper' });
|
||||||
|
await expect(community.getByRole('status')).toContainText('Member joined');
|
||||||
|
await expect(community.getByRole('status')).toContainText('Friend request');
|
||||||
|
await expect(community.getByRole('status')).not.toContainText(
|
||||||
|
'Message received',
|
||||||
|
);
|
||||||
|
await expect(
|
||||||
|
dialog
|
||||||
|
.locator('label')
|
||||||
|
.filter({ hasText: 'Messages only' })
|
||||||
|
.getByRole('status'),
|
||||||
|
).toHaveCount(0);
|
||||||
|
await community.getByRole('checkbox').check();
|
||||||
|
await dialog
|
||||||
|
.getByRole('button', { name: 'Add plugin processor', exact: true })
|
||||||
|
.click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('status').filter({ hasText: warning }),
|
||||||
|
).toHaveCount(1);
|
||||||
|
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||||
|
await page.reload();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('status').filter({ hasText: warning }),
|
||||||
|
).toHaveCount(1);
|
||||||
|
await page.getByRole('combobox').first().click();
|
||||||
|
await page
|
||||||
|
.getByRole('option', { name: 'Community adapter', exact: true })
|
||||||
|
.click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('status').filter({ hasText: warning }),
|
||||||
|
).toHaveCount(0);
|
||||||
|
await page.getByRole('combobox').first().click();
|
||||||
|
await page
|
||||||
|
.getByRole('option', { name: 'Message adapter', exact: true })
|
||||||
|
.click();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('status').filter({ hasText: warning }),
|
||||||
|
).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('new component choices warn for missing and wildcard events but allow creation and binding', async ({
|
||||||
|
page,
|
||||||
|
}, testInfo) => {
|
||||||
|
await setup(page);
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'Add plugin processor', exact: true })
|
||||||
|
.click();
|
||||||
|
const dialog = page.getByRole('dialog');
|
||||||
|
await dialog
|
||||||
|
.getByRole('tab', { name: 'New configuration', exact: true })
|
||||||
|
.click();
|
||||||
|
for (const name of ['Community helper', 'Group observer', 'All observer']) {
|
||||||
|
await expect(
|
||||||
|
dialog
|
||||||
|
.getByRole('button', { name: new RegExp(name) })
|
||||||
|
.getByRole('status'),
|
||||||
|
).toContainText(warning);
|
||||||
|
}
|
||||||
|
await expect(
|
||||||
|
dialog.getByRole('button', { name: /Messages only/ }).getByRole('status'),
|
||||||
|
).toHaveCount(0);
|
||||||
|
await dialog.getByRole('button', { name: /Community helper/ }).click();
|
||||||
|
await dialog.screenshot({
|
||||||
|
path: testInfo.outputPath('compatibility-dialog.png'),
|
||||||
|
});
|
||||||
|
await dialog
|
||||||
|
.getByRole('button', { name: 'Create and bind', exact: true })
|
||||||
|
.click();
|
||||||
|
await expect(dialog).toHaveCount(0);
|
||||||
|
await expect(
|
||||||
|
page.getByRole('status').filter({ hasText: warning }),
|
||||||
|
).toHaveCount(1);
|
||||||
|
await page
|
||||||
|
.locator('section[aria-labelledby="plugin-subscriptions-title"]')
|
||||||
|
.screenshot({ path: testInfo.outputPath('compatibility-bindings.png') });
|
||||||
|
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||||
|
await page.reload();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('status').filter({ hasText: warning }),
|
||||||
|
).toHaveCount(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fully supported processor choices have no compatibility warning', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await setup(page, [
|
||||||
|
'message.received',
|
||||||
|
'group.member_joined',
|
||||||
|
'friend.request_received',
|
||||||
|
]);
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'Add plugin processor', exact: true })
|
||||||
|
.click();
|
||||||
|
const dialog = page.getByRole('dialog');
|
||||||
|
await expect(
|
||||||
|
dialog
|
||||||
|
.locator('label')
|
||||||
|
.filter({ hasText: 'Community helper' })
|
||||||
|
.getByRole('status'),
|
||||||
|
).toHaveCount(0);
|
||||||
|
await dialog
|
||||||
|
.getByRole('tab', { name: 'New configuration', exact: true })
|
||||||
|
.click();
|
||||||
|
await expect(
|
||||||
|
dialog.getByRole('button', { name: /Community helper/ }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
dialog
|
||||||
|
.getByRole('button', { name: /Community helper/ })
|
||||||
|
.getByRole('status'),
|
||||||
|
).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const events of [
|
||||||
|
undefined,
|
||||||
|
['message.received'],
|
||||||
|
['message.received', 'message.edited'],
|
||||||
|
]) {
|
||||||
|
test(`other events entry follows adapter events: ${events?.join(',') ?? 'legacy'}`, async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await setup(page, events);
|
||||||
|
await page
|
||||||
|
.getByRole('button', { name: 'Add behavior', exact: true })
|
||||||
|
.click();
|
||||||
|
const entry = page.getByRole('menuitem', {
|
||||||
|
name: /Configure another event/,
|
||||||
|
});
|
||||||
|
if (events?.includes('message.edited')) {
|
||||||
|
await expect(entry).toBeVisible();
|
||||||
|
await entry.hover();
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: /Message edited/ }),
|
||||||
|
).toBeVisible();
|
||||||
|
} else {
|
||||||
|
await expect(entry).toHaveCount(0);
|
||||||
|
await expect(
|
||||||
|
page.getByRole('menuitem', { name: /Reply to messages/ }),
|
||||||
|
).toBeVisible();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import test from 'node:test';
|
||||||
|
import ts from 'typescript';
|
||||||
|
|
||||||
|
const source = fs.readFileSync(
|
||||||
|
new URL(
|
||||||
|
'../../src/app/home/components/event-patterns/event-pattern-groups.ts',
|
||||||
|
import.meta.url,
|
||||||
|
),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const exports = {};
|
||||||
|
new Function(
|
||||||
|
'exports',
|
||||||
|
ts.transpileModule(source, {
|
||||||
|
compilerOptions: { module: ts.ModuleKind.CommonJS },
|
||||||
|
}).outputText,
|
||||||
|
)(exports);
|
||||||
|
|
||||||
|
for (const [supported, required, covered] of [
|
||||||
|
['message.received', 'message.received', true],
|
||||||
|
['message.received', 'group.member_joined', false],
|
||||||
|
['message.received', 'message.*', false],
|
||||||
|
['message.received', '*', false],
|
||||||
|
['group.*', 'group.member_joined', true],
|
||||||
|
['group.*', 'group.*', true],
|
||||||
|
['group.*', 'groupish.member_joined', false],
|
||||||
|
['group.*', '*', false],
|
||||||
|
['*', 'group.*', true],
|
||||||
|
['*', '*', true],
|
||||||
|
]) {
|
||||||
|
test(`${supported} ${covered ? 'covers' : 'does not cover'} ${required}`, () => {
|
||||||
|
assert.equal(exports.eventPatternCovers(supported, required), covered);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user