Show persistent timestamps for operation and teacher messages
This commit is contained in:
27
tests/e2e/fixtures/message-timestamps.tsx
Normal file
27
tests/e2e/fixtures/message-timestamps.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { CodingConversationTimeline } from '../../../src/pages/Chat/CodingConversationTimeline';
|
||||
import { TeacherChatPanel } from '../../../src/pages/Chat/TeacherChatPanel';
|
||||
import { codingConversationStore } from '../../../src/stores/coding-conversations';
|
||||
import { createProductSnapshot } from '../../fixtures/coding-conversation-product-fixtures';
|
||||
import type { TeacherDefinition, TeacherTopic } from '../../../shared/coding-teacher';
|
||||
import '../../../src/styles/globals.css';
|
||||
import '../../../src/pages/Chat/classroom-workspace.css';
|
||||
|
||||
const sent = '2026-09-28T08:01:02.000Z', finished = '2026-09-28T08:01:42.000Z';
|
||||
const snapshot = createProductSnapshot('timestamp-fixture', 1);
|
||||
snapshot.run = { status: 'idle' };
|
||||
snapshot.nodes = [
|
||||
{ kind: 'message', id: 'u', role: 'user', status: 'complete', timestamp: Date.parse(sent), blocks: [{ kind: 'text', id: 'ut', status: 'complete', text: '我想做一个会长大的小宠物。' }] },
|
||||
{ kind: 'message', id: 'a', role: 'assistant', status: 'complete', timestamp: Date.parse(finished), blocks: [{ kind: 'text', id: 'at', status: 'complete', text: '小宠物的家搭好啦。你可以试着点一下喂食按钮,看看它的变化。' }] },
|
||||
];
|
||||
codingConversationStore.getState().primeConversation(snapshot);
|
||||
const definition: TeacherDefinition = { schema_version: 1, config_id: 'teacher-fixture', teacher_id: 'teacher-fixture', name: '设计老师', description: '', avatar_id: 'avatar-06', welcome_message: '', suggested_questions: [], system_prompt: '', skills: [], model: { model_id: 'fixture', reasoning_choice: { mode: 'default' } }, limits: { max_input_tokens: 8000, max_output_tokens: 1000 } };
|
||||
const topic: TeacherTopic = { schemaVersion: 1, revision: 1, id: 'teacher-fixture', accountId: 'fixture', projectId: 'fixture', sourceConversationId: 'timestamp-fixture', definition, version: 1, conversation: { agentId: 'teacher-fixture', segmentTurns: 1, discussions: {} }, createdAt: sent, updatedAt: finished, requests: [{ id: 'r', createdAt: sent, finishedAt: finished, text: '我想让它长大以后变得很特别。', response: '先想一个你最希望它学会的小本领吧。你希望它会帮你做什么?', suggestedQuestions: ['我想让它陪我讲故事。', '我想让它记住我的习惯。'], references: [], sourceCursor: { workerGeneration: 1, seq: 1 }, sourceCapturedAt: sent, includedSourceMessageIds: [], omittedMessages: 0, status: 'completed' }] };
|
||||
(window as typeof window & { __timestampTopic: TeacherTopic }).__timestampTopic = topic;
|
||||
const agent = { teacher_id: definition.config_id!, version: 1, is_default: true, definition };
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<div className="classroom-workspace min-h-screen bg-white" style={{ display: 'grid', gridTemplateColumns: innerWidth >= 768 ? 'minmax(0, 1fr) 420px' : 'minmax(0, 1fr)' }}>
|
||||
<main className="min-w-0 p-6" aria-label="操作对话"><h1 className="mb-8 text-lg">操作对话</h1><CodingConversationTimeline conversationId="timestamp-fixture" /></main>
|
||||
<div className="consultation-pane h-[850px] border-l"><TeacherChatPanel projectId="fixture" sourceId="timestamp-fixture" selectedAgent={agent} catalogState={{ config: { enabled: true, definition }, catalog: { items: [agent] } }} /></div>
|
||||
</div>,
|
||||
);
|
||||
79
tests/e2e/message-timestamps.spec.ts
Normal file
79
tests/e2e/message-timestamps.spec.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { createServer, type ViteDevServer } from 'vite';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
let server: ViteDevServer;
|
||||
let baseUrl: string;
|
||||
test.use({ headless: true, channel: process.env.MAKELORE_LAYOUT_BROWSER_CHANNEL, video: 'off' });
|
||||
test.beforeAll(async () => {
|
||||
server = await createServer({
|
||||
configFile: false, cacheDir: 'node_modules/.vite-message-timestamps',
|
||||
resolve: { alias: { '@': resolve('src') } }, esbuild: { jsx: 'automatic' },
|
||||
server: { host: '127.0.0.1', port: 0 },
|
||||
plugins: [{
|
||||
name: 'isolated-message-timestamps', enforce: 'pre',
|
||||
load(id) {
|
||||
if (id.endsWith('/src/lib/coding-teacher.ts')) return `
|
||||
export function teacherTopicsPath() { return '/fixture'; }
|
||||
export function legacyTopicBase(base) { return base; }
|
||||
export const teacherApi = {
|
||||
conversation: async () => ({ topic: window.__timestampTopic, before: null }),
|
||||
seenConversation: async () => window.__timestampTopic,
|
||||
events: async () => ({ close() {}, addEventListener() {} }),
|
||||
};
|
||||
`;
|
||||
if (id.endsWith('/src/lib/host-events.ts')) return 'export function subscribeHostEvent() { return () => {}; }';
|
||||
if (id.endsWith('/src/lib/host-api.ts')) return `
|
||||
export async function createHostEventSource() { return { close() {}, addEventListener() {} }; }
|
||||
export async function ensureHostApiToken() { return 'fixture'; }
|
||||
export function getHostApiBase() { return ''; }
|
||||
export function reportDesktopActivity() {}
|
||||
export function setDesktopBackgroundLease() {}
|
||||
export async function hostApiFetch() { return {}; }
|
||||
export async function hostApiFetchBytes() { return new Uint8Array(); }
|
||||
`;
|
||||
},
|
||||
configureServer(vite) {
|
||||
vite.middlewares.use((req, res, next) => {
|
||||
if (req.url !== '/' && !req.url?.startsWith('/?')) return next();
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end('<html><head><meta charset="utf-8"></head><body><div id="root"></div><script type="module" src="/tests/e2e/fixtures/message-timestamps.tsx"></script></body></html>');
|
||||
});
|
||||
},
|
||||
}],
|
||||
});
|
||||
await server.listen();
|
||||
baseUrl = server.resolvedUrls!.local[0];
|
||||
});
|
||||
test.afterAll(async () => { await server?.close(); });
|
||||
test.beforeEach(async ({ page }) => {
|
||||
page.on('pageerror', error => { throw error; });
|
||||
await page.addInitScript(() => {
|
||||
const scope = window as typeof window & { __communityCalls: unknown[][] };
|
||||
scope.__communityCalls = [];
|
||||
Object.defineProperty(window, 'electron', { value: { ipcRenderer: {
|
||||
invoke: async (...args: unknown[]) => { scope.__communityCalls.push(args); return undefined; },
|
||||
} } });
|
||||
});
|
||||
});
|
||||
|
||||
for (const width of [375, 1280]) {
|
||||
test(`both chat surfaces show stable local times at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await page.goto(baseUrl);
|
||||
await expect(page.locator('time')).toHaveCount(4);
|
||||
const times = await page.locator('time').evaluateAll(elements => elements.map(element => ({ time: element.getAttribute('datetime'), title: element.getAttribute('title') })));
|
||||
expect(times.map(item => item.time)).toEqual(['2026-09-28T08:01:02.000Z', '2026-09-28T08:01:42.000Z', '2026-09-28T08:01:02.000Z', '2026-09-28T08:01:42.000Z']);
|
||||
for (const item of times) expect(item.title).toMatch(/^2026年9月28日 \d{2}:\d{2}:(02|42)$/);
|
||||
for (const time of await page.locator('time').all()) {
|
||||
await expect(time).toHaveCSS('font-size', '11px');
|
||||
expect(await time.evaluate(el => el.scrollWidth <= el.clientWidth)).toBe(true);
|
||||
}
|
||||
await expect(page.getByRole('button', { name: '我想让它陪我讲故事。' })).toBeEnabled();
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
|
||||
await page.screenshot({ path: test.info().outputPath(`message-timestamps-${width}.png`), fullPage: true });
|
||||
await page.reload();
|
||||
await expect(page.locator('time')).toHaveCount(4);
|
||||
expect(await page.locator('time').evaluateAll(elements => elements.map(element => ({ time: element.getAttribute('datetime'), title: element.getAttribute('title') })))).toEqual(times);
|
||||
});
|
||||
}
|
||||
@@ -226,6 +226,7 @@ async function installCodingFirstChatHost(
|
||||
kind: 'message',
|
||||
id: 'message-user-e2e',
|
||||
sourceEntryId: 'entry-user-e2e',
|
||||
timestamp: Date.parse(now),
|
||||
role: 'user',
|
||||
status: 'complete',
|
||||
blocks: [{ kind: 'text', id: 'message-user-e2e:content:0', text: 'Durable user fork source', status: 'complete' }],
|
||||
@@ -582,7 +583,7 @@ async function installCodingFirstChatHost(
|
||||
if (action === 'messages' && method === 'POST') {
|
||||
const suggestions = body!.intent === 'suggestions';
|
||||
const response = suggestions ? '我们可以从你最近试过的地方聊起。' : body!.intent === 'guided-help' ? '你最近做的哪一步,让你停下来想了一会儿?' : '先理解状态如何随点击变化,再修改代码。';
|
||||
consultationTopics[agentId] = {...current,revision:Number(current.revision)+1,requests:[...(current.requests as unknown[]),{id:body!.requestId,projectId:body!.projectId,sourceConversationId:body!.sourceConversationId,text:body!.text,intent:body!.intent,references:body!.references??[],createdAt:new Date().toISOString(),sourceCursor:{workerGeneration:1,seq:1},sourceCapturedAt:now,includedSourceMessageIds:[],omittedMessages:0,truncatedMessages:1,status:'completed',response,...(suggestions?{suggestedQuestions:['怎样观察别人玩游戏?','我该先试哪个想法?']}:{})}]};
|
||||
consultationTopics[agentId] = {...current,revision:Number(current.revision)+1,requests:[...(current.requests as unknown[]),{id:body!.requestId,projectId:body!.projectId,sourceConversationId:body!.sourceConversationId,text:body!.text,intent:body!.intent,references:body!.references??[],createdAt:now,finishedAt:new Date(Date.parse(now)+40000).toISOString(),sourceCursor:{workerGeneration:1,seq:1},sourceCapturedAt:now,includedSourceMessageIds:[],omittedMessages:0,truncatedMessages:1,status:'completed',response,...(suggestions?{suggestedQuestions:['怎样观察别人玩游戏?','我该先试哪个想法?']}:{})}]};
|
||||
const last = (consultationTopics[agentId].requests as Record<string, unknown>[]).at(-1)!;
|
||||
Object.assign(last, consultationReply);
|
||||
if (state.nextTeacherReplyIncomplete) {
|
||||
@@ -1914,6 +1915,11 @@ test('project consultations preserve student drafts and switch between work and
|
||||
await teacher.getByRole('textbox',{name:'向智能体提问'}).fill('');
|
||||
await teacher.locator('form .consultation-hint').click();
|
||||
await expect(teacher.getByText('我们可以从你最近试过的地方聊起。')).toBeVisible();
|
||||
const sentTime = teacher.locator('time').nth(0), replyTime = teacher.locator('time').nth(1);
|
||||
await expect(sentTime).toHaveAttribute('aria-label', /^发送时间:/);
|
||||
await expect(replyTime).toHaveAttribute('aria-label', /^回复完成时间:/);
|
||||
const originalReplyTime = await replyTime.getAttribute('datetime');
|
||||
expect(Date.parse(originalReplyTime!) - Date.parse((await sentTime.getAttribute('datetime'))!)).toBe(40000);
|
||||
await expect(teacher.getByTestId('teacher-discussion')).toHaveCount(0);
|
||||
await teacher.getByRole('textbox',{name:'向智能体提问'}).fill('还没说完的困惑');
|
||||
await expect(teacher.locator('form .consultation-hint')).toHaveCount(0);
|
||||
@@ -1965,6 +1971,7 @@ test('project consultations preserve student drafts and switch between work and
|
||||
await page.getByRole('button',{name:'与朋友聊天',exact:true}).click();await expect(teacher).toHaveCount(0);
|
||||
await page.getByRole('button',{name:'与朋友聊天',exact:true}).click();
|
||||
await expect(page.getByText('先理解状态如何随点击变化,再修改代码。',{exact:true})).toBeVisible();
|
||||
await expect(teacher.locator('time').nth(1)).toHaveAttribute('datetime', originalReplyTime!);
|
||||
await expect(teacher.getByRole('textbox',{name:'向智能体提问'})).toHaveValue('还没说完的困惑');
|
||||
} finally {await releaseSnapshot(electronApp);}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user