mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
test(assistant): shared card tests, wiki rows and ratchets after the card packages
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* ask for a change only for someone who may make it, and the answer's own
|
||||
* when none is left for a reader.
|
||||
*/
|
||||
import type { AssistantMessage } from '@trek/shared';
|
||||
import type { AssistantMessage, TrekCard } from '@trek/shared';
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
|
||||
import { followUpsFor } from './assistantFollowUpRules';
|
||||
@@ -19,7 +19,7 @@ const answer: AssistantMessage = {
|
||||
parts: [
|
||||
{ type: 'text', text: 'Here is your year.' },
|
||||
{ type: 'tool-call', toolCallId: 'r1', toolName: 'get_vacay_plan', args: {}, result: {} },
|
||||
{ type: 'data-trek-card', data: { toolCallId: 'r1', card: { kind: 'vacay' } } },
|
||||
{ type: 'data-trek-card', data: { toolCallId: 'r1', card: { kind: 'vacay' } as TrekCard } },
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -304,8 +304,8 @@ describe('messageBlocks: found places and weather', () => {
|
||||
});
|
||||
|
||||
describe('messageBlocks: cards', () => {
|
||||
const ROUTE: TrekCard = { kind: 'tourRoute' };
|
||||
const LIST: TrekCard = { kind: 'tourList' };
|
||||
const ROUTE = { kind: 'tourRoute' } as TrekCard;
|
||||
const LIST = { kind: 'tourList' } as TrekCard;
|
||||
const card = (toolCallId: string, value: TrekCard | null): AssistantMessagePart => ({
|
||||
type: 'data-trek-card',
|
||||
data: { toolCallId, card: value },
|
||||
|
||||
@@ -30,11 +30,15 @@ describe('assistantShowOnMap', () => {
|
||||
|
||||
it('FE-ASSIST-SHOW-003: no target where nothing shows on the map; a place without a spot opens the planner', () => {
|
||||
expect(showOnMapTarget(applied('packing', [2], null))).toBeNull();
|
||||
expect(showOnMapTarget(applied('tour', [2], 3))).toBeNull();
|
||||
expect(showOnMapTarget(applied('tour', [], 3))).toBeNull();
|
||||
expect(showOnMapTarget(applied('memory', [], null))).toBeNull();
|
||||
expect(searchOf(applied('place', [], null))).toBe('');
|
||||
});
|
||||
|
||||
it('FE-ASSIST-SHOW-005: an applied tour goes to its line on the plan, by the place it is', () => {
|
||||
expect(searchOf(applied('tour', [2], 3))).toBe('?place=2');
|
||||
});
|
||||
|
||||
it('FE-ASSIST-SHOW-004: a tab of the trip goes as ?tab= with what the tab reads beside it', () => {
|
||||
const expense = plannerSearchOf({ kind: 'tab', tab: 'finanzplan', params: { expense: '7' } });
|
||||
expect(expense).toBe('?tab=finanzplan&expense=7');
|
||||
|
||||
@@ -17,6 +17,8 @@ vi.mock('./cardViews', async (importOriginal) => {
|
||||
const { createElement } = await import('react');
|
||||
const views = {
|
||||
...actual.CARD_VIEWS,
|
||||
// A kind left without a view, whatever its package draws.
|
||||
vacay: {},
|
||||
tourList: {
|
||||
Block: function Block({ cards }: { cards: readonly { toolCallId: string }[] }) {
|
||||
return createElement('p', null, `${cards.length} tour lists`);
|
||||
@@ -50,12 +52,12 @@ afterEach(() => vi.restoreAllMocks());
|
||||
|
||||
describe('AssistantCardBlock', () => {
|
||||
it('FE-AI-CARDBLOCK-001: the kind draws the whole block', () => {
|
||||
render(<AssistantCardBlock block={block({ kind: 'tourList' }, 2)} message={MESSAGE} />);
|
||||
render(<AssistantCardBlock block={block({ kind: 'tourList' } as TrekCard, 2)} message={MESSAGE} />);
|
||||
expect(screen.getByText('2 tour lists')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('FE-AI-CARDBLOCK-002: a kind not drawn yet shows nothing', () => {
|
||||
const { container } = render(<AssistantCardBlock block={block({ kind: 'vacay' })} message={MESSAGE} />);
|
||||
const { container } = render(<AssistantCardBlock block={block({ kind: 'vacay' } as TrekCard)} message={MESSAGE} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
@@ -63,7 +65,7 @@ describe('AssistantCardBlock', () => {
|
||||
const logged = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const { container } = render(
|
||||
<div>
|
||||
<AssistantCardBlock block={block({ kind: 'tourRoute' })} message={MESSAGE} />
|
||||
<AssistantCardBlock block={block({ kind: 'tourRoute' } as TrekCard)} message={MESSAGE} />
|
||||
<span>the answer</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -64,9 +64,9 @@ vi.mock('./cardViews', async (importOriginal) => {
|
||||
|
||||
const TRIP = 4;
|
||||
const INITIAL = useAssistantStore.getState();
|
||||
const ROUTE: TrekCard = { kind: 'tourRoute' };
|
||||
const LIST: TrekCard = { kind: 'tourList' };
|
||||
const BROKEN: TrekCard = { kind: 'dayRoute' };
|
||||
const ROUTE = { kind: 'tourRoute' } as TrekCard;
|
||||
const LIST = { kind: 'tourList' } as TrekCard;
|
||||
const BROKEN = { kind: 'dayRoute' } as TrekCard;
|
||||
|
||||
function preview(toolCallId: string, subject: string): AssistantChangePreview {
|
||||
return {
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
ASSISTANT_CHANGE_VERB_KEYS,
|
||||
type AssistantApplied,
|
||||
type AssistantChangeVerbKey,
|
||||
type TrekCard,
|
||||
type TrekCardKind,
|
||||
type TrekCardOf,
|
||||
trekCardSchema,
|
||||
} from '@trek/shared';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
@@ -32,6 +34,10 @@ import type { CardLook } from './cardTypes';
|
||||
import { CARD_VIEWS } from './cardViews';
|
||||
|
||||
const KINDS = trekCardSchema.options.map((option) => option.shape.kind.value) as TrekCardKind[];
|
||||
/** A vacation card cut down to its kind: the layout and map rules read nothing else of it. */
|
||||
const VACAY = { kind: 'vacay' } as TrekCard;
|
||||
/** A day route cut down to its kind, for the same reason. */
|
||||
const DAY_ROUTE = { kind: 'dayRoute' } as TrekCardOf<'dayRoute'>;
|
||||
|
||||
describe('cardLooks', () => {
|
||||
it('FE-AI-LOOK-001: every verb is named by its own key', () => {
|
||||
@@ -46,17 +52,22 @@ describe('cardLooks', () => {
|
||||
expect(STEP_LOOKS.get_trip_summary?.stepKey).toBe('assistant.step.reading');
|
||||
});
|
||||
|
||||
it('FE-AI-LOOK-003: only a place, a stop, a note and a booking show on the map, as before the cards', () => {
|
||||
it('FE-AI-LOOK-003: a place, a stop, a note, a booking and a stay show on the map; a kept note never', () => {
|
||||
const onMap = Object.entries(ENTITY_LOOK).flatMap(([entity, look]) => (look.onMap ? [entity] : []));
|
||||
expect(onMap.sort()).toEqual(['assignment', 'day_note', 'place', 'reservation']);
|
||||
expect(Object.values(ENTITY_LOOK).every((look) => !look.open)).toBe(true);
|
||||
// The core's own; a part of TREK may add its entities (a stay shows its place).
|
||||
expect(onMap).toEqual(expect.arrayContaining(['assignment', 'day_note', 'place', 'reservation', 'accommodation']));
|
||||
expect(onMap).not.toContain('memory');
|
||||
expect(onMap).not.toContain('other');
|
||||
for (const entity of ['place', 'assignment', 'day_note', 'memory', 'other'] as const) {
|
||||
expect(ENTITY_LOOK[entity].open).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('FE-AI-LOOK-004: every kind has a look and a view; a pending view groups by kind and gives way to nothing', () => {
|
||||
expect(Object.keys(CARD_LOOKS).sort()).toEqual([...KINDS].sort());
|
||||
expect(Object.keys(CARD_VIEWS).sort()).toEqual([...KINDS].sort());
|
||||
expect(CARD_RULES.group({ kind: 'vacay' })).toBe('vacay');
|
||||
expect(CARD_RULES.yieldsToChange({ kind: 'expenses' }, { kind: 'payments' })).toBe(false);
|
||||
expect(CARD_RULES.group(VACAY)).toBe('vacay');
|
||||
expect(CARD_RULES.yieldsToChange({ kind: 'costPerPerson' } as TrekCard, VACAY)).toBe(false);
|
||||
});
|
||||
|
||||
it('FE-AI-LOOK-005: a kind draws its lines and points on the map through its look', () => {
|
||||
@@ -66,10 +77,10 @@ describe('cardLooks', () => {
|
||||
],
|
||||
ghosts: (): GhostPoint[] => [{ id: 'via', lat: 45.1, lng: 7.2, name: 'Col', variant: 'point' }],
|
||||
};
|
||||
expect(look.lines?.({ kind: 'dayRoute' }, 'before')).toEqual([
|
||||
expect(look.lines?.(DAY_ROUTE, 'before')).toEqual([
|
||||
{ id: 'day:before', lines: [[[45.1, 7.2]]], color: 'var(--accent)', dashed: true },
|
||||
]);
|
||||
expect(look.ghosts?.({ kind: 'dayRoute' })[0]?.variant).toBe('point');
|
||||
expect(look.ghosts?.(DAY_ROUTE)[0]?.variant).toBe('point');
|
||||
});
|
||||
|
||||
it("FE-AI-LOOK-006: the layout rules and the map read a kind's look, nothing for one that says nothing", () => {
|
||||
@@ -78,19 +89,17 @@ describe('cardLooks', () => {
|
||||
route.yieldsToChange = (_answer, change) => change.kind === 'dayRoute';
|
||||
route.lines = (_card, use) => [{ id: `day:${use}`, lines: [], color: 'var(--accent)' }];
|
||||
try {
|
||||
expect(CARD_RULES.group({ kind: 'dayRoute' })).toBe('routes');
|
||||
expect(CARD_RULES.yieldsToChange({ kind: 'dayRoute' }, { kind: 'dayRoute' })).toBe(true);
|
||||
expect(CARD_MAP_LOOK.lines({ kind: 'dayRoute' }, 'look')).toEqual([
|
||||
{ id: 'day:look', lines: [], color: 'var(--accent)' },
|
||||
]);
|
||||
expect(CARD_RULES.group(DAY_ROUTE)).toBe('routes');
|
||||
expect(CARD_RULES.yieldsToChange(DAY_ROUTE, DAY_ROUTE)).toBe(true);
|
||||
expect(CARD_MAP_LOOK.lines(DAY_ROUTE, 'look')).toEqual([{ id: 'day:look', lines: [], color: 'var(--accent)' }]);
|
||||
} finally {
|
||||
delete route.group;
|
||||
delete route.yieldsToChange;
|
||||
delete route.lines;
|
||||
}
|
||||
expect(CARD_MAP_LOOK.lines({ kind: 'dayRoute' }, 'look')).toEqual([]);
|
||||
expect(CARD_MAP_LOOK.ghosts({ kind: 'vacay' })).toEqual([]);
|
||||
expect(CARD_MAP_LOOK.hidesEffectGhost({ kind: 'vacay' })).toBe(false);
|
||||
expect(CARD_MAP_LOOK.lines(DAY_ROUTE, 'look')).toEqual([]);
|
||||
expect(CARD_MAP_LOOK.ghosts(VACAY)).toEqual([]);
|
||||
expect(CARD_MAP_LOOK.hidesEffectGhost(VACAY)).toBe(false);
|
||||
});
|
||||
|
||||
it('FE-AI-LOOK-007: a verb or entity only a newer server knows looks like a plain update or an other', () => {
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
*
|
||||
* The links the chat opens the planner with while it is on screen: another
|
||||
* tab of the trip switches there and leaves the URL, and the parts' own links
|
||||
* (tours, bookings, road trip) take nothing yet.
|
||||
* (tours, bookings, road trip) are each handed the planner, the workspace and
|
||||
* the query (each part's own test covers what it does with them).
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import type { ReactNode } from 'react';
|
||||
@@ -12,6 +13,11 @@ import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { useAssistantEntityLinks } from './useAssistantEntityLinks';
|
||||
|
||||
const parts = vi.hoisted(() => ({ tour: vi.fn(), booking: vi.fn(), roadtrip: vi.fn() }));
|
||||
vi.mock('./deepLinks/useTourDeepLink', () => ({ useTourDeepLink: parts.tour }));
|
||||
vi.mock('./deepLinks/useBookingDeepLink', () => ({ useBookingDeepLink: parts.booking }));
|
||||
vi.mock('./deepLinks/useRoadtripDeepLink', () => ({ useRoadtripDeepLink: parts.roadtrip }));
|
||||
|
||||
type Options = Parameters<typeof useAssistantEntityLinks>[0];
|
||||
|
||||
function setup(path: string) {
|
||||
@@ -37,9 +43,14 @@ describe('useAssistantEntityLinks', () => {
|
||||
expect(hook.result.current.search).toBe('?expense=7');
|
||||
});
|
||||
|
||||
it("FE-PLANNER-ENTITYLINK-002: the parts' own links take nothing yet, and nothing switches without a tab", () => {
|
||||
it("FE-PLANNER-ENTITYLINK-002: each part's own link gets the planner, the workspace and the query; nothing switches without a tab", () => {
|
||||
const { planner, hook } = setup('/trips/5?tour=12');
|
||||
expect(planner.handleTabChange).not.toHaveBeenCalled();
|
||||
expect(hook.result.current.search).toBe('?tour=12');
|
||||
for (const part of [parts.tour, parts.booking, parts.roadtrip]) {
|
||||
const links = part.mock.lastCall?.[0] as { planner: unknown; searchParams: URLSearchParams };
|
||||
expect(links.planner).toBe(planner);
|
||||
expect(links.searchParams.get('tour')).toBe('12');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -71,8 +71,8 @@ describe('assistantMessages: cards in an answer', () => {
|
||||
expect(messages[0]?.parts.map((p) => p.type)).toEqual(['data-trek-weather']);
|
||||
});
|
||||
|
||||
const ROUTE: TrekCard = { kind: 'tourRoute' };
|
||||
const LIST: TrekCard = { kind: 'tourList' };
|
||||
const ROUTE = { kind: 'tourRoute' } as TrekCard;
|
||||
const LIST = { kind: 'tourList' } as TrekCard;
|
||||
const cardsOf = (message: AssistantMessage | undefined) =>
|
||||
message?.parts.flatMap((p) => (p.type === 'data-trek-card' ? [p.data] : [])) ?? [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user