Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/desktop/e2e/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ export const test = base.extend<{
parentRemovalWindow: Page;
railRenderWindow: Page;
promptRailWindow: Page;
partialHistoryWindow: Page;
promptRailMotionWindow: Page;
requestHeaderRowWindow: Page;
newTaskTargetWindow: Page;
Expand Down Expand Up @@ -588,6 +589,17 @@ export const test = base.extend<{
showWindow: true,
}, use);
},
// A transcript larger than the bounded Desktop range. Clicking an unloaded
// prompt exercises the real load-around path and its partial-history UI.
partialHistoryWindow: async ({}, use) => {
await withE2eWindow({
seed: false,
readinessSelector: '[data-turn-id]',
e2eFixtureScenario: 'chat-partial-history',
locale: 'zh',
showWindow: true,
}, use);
},
// The same transcript, scrolling the way the shipped app scrolls. Separate
// from `promptRailWindow` because it is only the jump that needs a scroll
// still in flight, and paying for one everywhere costs several seconds per
Expand Down
137 changes: 137 additions & 0 deletions apps/desktop/e2e/partial-history-notice.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import type { Page } from '@playwright/test';
import { expect, test } from './fixtures';

const NOTICE = '.maka-transcript-history-controls';

async function waitForPaint(page: Page): Promise<void> {
await page.evaluate(() => new Promise<void>((resolve) => {
requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
}));
}

async function noticePresentation(page: Page) {
return page.locator(NOTICE).evaluate((notice) => {
const style = getComputedStyle(notice);
const box = notice.getBoundingClientRect();
const composer = document.querySelector('.maka-composer-astryx');
if (!composer) throw new Error('the composer is missing');
const composerBox = composer.getBoundingClientRect();
return {
backgroundColor: style.backgroundColor,
borderWidths: [
style.borderTopWidth,
style.borderRightWidth,
style.borderBottomWidth,
style.borderLeftWidth,
],
display: style.display,
flexWrap: style.flexWrap,
justifyContent: style.justifyContent,
widthDelta: Math.abs(box.width - composerBox.width),
centerDelta: Math.abs(
(box.left + box.right) / 2 - (composerBox.left + composerBox.right) / 2,
),
fitsViewport: box.left >= 0 && box.right <= document.documentElement.clientWidth,
hasHorizontalOverflow: notice.scrollWidth > notice.clientWidth,
};
});
}

test('partial history is a quiet reading-column control with neutral rail ticks', async ({
partialHistoryWindow: page,
}) => {
await page.setViewportSize({ width: 1_400, height: 800 });
await expect(page.locator(NOTICE)).toHaveCount(0);

const firstPrompt = page.locator(
'.maka-prompt-rail-tick[data-prompt-turn-id="turn-partial-history-1"]',
);
await expect(firstPrompt).toBeVisible();
await firstPrompt.click();

const notice = page.locator(NOTICE);
await expect(notice).toBeVisible();
await expect(notice).toContainText('正在查看较早的消息');
await expect(notice.getByRole('button', { name: '返回最新消息' })).toBeVisible();
await expect(notice).not.toContainText(/保存|加载/);

const regular = await noticePresentation(page);
expect(regular).toEqual({
backgroundColor: 'rgba(0, 0, 0, 0)',
borderWidths: ['0px', '0px', '0px', '0px'],
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'center',
widthDelta: expect.any(Number),
centerDelta: expect.any(Number),
fitsViewport: true,
hasHorizontalOverflow: false,
});
expect(regular.widthDelta).toBeLessThanOrEqual(1);
expect(regular.centerDelta).toBeLessThanOrEqual(1);

await page.mouse.move(0, 0);
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur());
const railPresentation = await page.evaluate(() => {
const ticks = [...document.querySelectorAll<HTMLElement>('.maka-prompt-rail-tick')];
const presentation = (tick: HTMLElement) => {
const bar = tick.querySelector<HTMLElement>('.maka-prompt-rail-tick-bar');
if (!bar) throw new Error('a prompt rail tick is missing its bar');
const style = getComputedStyle(bar);
return {
backgroundColor: style.backgroundColor,
borderStyle: style.borderStyle,
borderWidth: style.borderWidth,
boxShadow: style.boxShadow,
};
};
const neutralPaint = ticks
.filter((tick) => tick.dataset.active !== 'true' && !tick.matches(':hover'))
.map(presentation);
const residentStyleRules = [...document.styleSheets].flatMap((sheet) =>
[...sheet.cssRules].filter((rule) => rule.cssText.includes('data-resident'))
);
return {
residentAttributeCount: document.querySelectorAll('[data-resident]').length,
residentStyleRuleCount: residentStyleRules.length,
neutralTickCount: neutralPaint.length,
neutralPaintCount: new Set(neutralPaint.map((paint) => JSON.stringify(paint))).size,
};
});
expect(railPresentation.residentAttributeCount).toBe(0);
expect(railPresentation.residentStyleRuleCount).toBe(0);
expect(railPresentation.neutralTickCount).toBeGreaterThan(1);
expect(railPresentation.neutralPaintCount).toBe(1);

await page.setViewportSize({ width: 520, height: 720 });
await waitForPaint(page);
const narrow = await noticePresentation(page);
expect(narrow.centerDelta).toBeLessThanOrEqual(1);
expect(narrow.fitsViewport).toBe(true);
expect(narrow.hasHorizontalOverflow).toBe(false);

await notice.getByRole('button', { name: '返回最新消息' }).click();
await expect(notice).toHaveCount(0);
await expect(
page.locator('[data-turn-id="turn-partial-history-8"]'),
).toBeVisible();
});
13 changes: 13 additions & 0 deletions apps/desktop/src/main/e2e-fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@ import {
LONG_SIDEBAR_PROJECT_ID,
LONG_SIDEBAR_PROJECT_NAME,
LONG_SIDEBAR_SESSION_PREFIX,
PARTIAL_HISTORY_SESSION_ID,
PROMPT_RAIL_SESSION_ID,
TURN_SESSION_ID,
writeSession,
} from './e2e-fixture/seed-helpers.js';
import {
partialHistoryMessages,
partialHistorySession,
promptRailMessages,
promptRailSession,
turnMessages,
Expand All @@ -59,6 +62,7 @@ const E2E_FIXTURE_SCENARIOS = new Set<E2eFixtureScenario>([
'turn-narrative',
'turn-narrative-browser',
'chat-prompt-rail',
'chat-partial-history',
'settings-data',
'settings-bots-onboarding',
'settings-general',
Expand Down Expand Up @@ -174,6 +178,8 @@ export function getE2eFixtureState(fixture: E2eFixture | null): E2eFixtureState
// this window scrolls smoothly is a per-launch choice (`scrollMotion`),
// because only the jump case needs it and it costs seconds of settling.
return { ...state, activeSessionId: PROMPT_RAIL_SESSION_ID, workbarCollapsed: true };
case 'chat-partial-history':
return { ...state, activeSessionId: PARTIAL_HISTORY_SESSION_ID, workbarCollapsed: true };
case 'settings-data':
return { ...state, activeSessionId: TURN_SESSION_ID, openSettingsSection: 'data' };
case 'settings-bots-onboarding':
Expand Down Expand Up @@ -225,6 +231,13 @@ export async function seedE2eFixture(input: {
if (scenario === 'chat-prompt-rail') {
await writeSession(input.workspaceRoot, promptRailSession(now), promptRailMessages(now));
}
if (scenario === 'chat-partial-history') {
await writeSession(
input.workspaceRoot,
partialHistorySession(now),
partialHistoryMessages(now),
);
}
if (scenario === 'sidebar-search-modal-open') {
for (const seed of longSidebarSessions(now)) {
await writeSession(input.workspaceRoot, seed.header, seed.messages);
Expand Down
43 changes: 43 additions & 0 deletions apps/desktop/src/main/e2e-fixture/scenarios-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import type { SessionHeader, StoredMessage } from '@maka/core/session';
import {
header,
PARTIAL_HISTORY_SESSION_ID,
PROMPT_RAIL_PROMPT_COUNT,
PROMPT_RAIL_SESSION_ID,
TURN_SESSION_ID,
Expand Down Expand Up @@ -148,3 +149,45 @@ export function promptRailMessages(now: number): StoredMessage[] {
}
return messages;
}

export function partialHistorySession(now: number): SessionHeader {
return header({
id: PARTIAL_HISTORY_SESSION_ID,
name: '超长对话历史范围示例',
connection: 'zai-live',
model: 'glm-5.1',
now,
lastMessageAt: now - 60_000,
});
}

/**
* Eight turns whose durable transcript is well over the Desktop range budget.
* The whitespace is stored but collapses when rendered, keeping this a useful
* visual fixture while forcing the initial open to contain only the latest
* contiguous range.
*/
export function partialHistoryMessages(now: number): StoredMessage[] {
const messages: StoredMessage[] = [];
const rangePadding = ' '.repeat(180 * 1024);
for (let index = 1; index <= 8; index += 1) {
const turnId = `turn-partial-history-${index}`;
const ts = now - (9 - index) * 60_000;
messages.push({
type: 'user',
id: `msg-partial-history-user-${index}`,
turnId,
ts,
text: `第 ${index} 个问题:请概括这一阶段的实现进展。`,
});
messages.push({
type: 'assistant',
id: `msg-partial-history-assistant-${index}`,
turnId,
ts: ts + 1_000,
text: `第 ${index} 阶段已经完成关键实现,并通过了对应验证。${rangePadding}`,
modelId: 'glm-5.1',
});
}
return messages;
}
1 change: 1 addition & 0 deletions apps/desktop/src/main/e2e-fixture/seed-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const E2E_FIXTURE_NOW = Date.UTC(2026, 4, 22, 3, 0, 0);

export const TURN_SESSION_ID = 'e2e-fixture-turn';
export const PROMPT_RAIL_SESSION_ID = 'e2e-fixture-prompt-rail';
export const PARTIAL_HISTORY_SESSION_ID = 'e2e-fixture-partial-history';
/** Exceeds both the 64-tick rail and 100-turn mounted-window bounds. */
export const PROMPT_RAIL_PROMPT_COUNT = 120;
export const LONG_SIDEBAR_SESSION_PREFIX = 'e2e-fixture-sidebar-long-';
Expand Down
1 change: 0 additions & 1 deletion apps/desktop/src/renderer/chat-message-surface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,6 @@ export function ChatMessageSurface({
onLoadEarlierHistory={onLoadEarlierHistory}
returnToLatest={hasNewerHistory ? {
title: transcriptCopy.partialHistoryTitle,
description: transcriptCopy.partialHistoryDescription,
label: transcriptCopy.returnLatest,
isPending: historyLoadPending,
onClick: onReturnToLatestHistory,
Expand Down
Loading