merge: integrate remote main
This commit is contained in:
@@ -18,6 +18,12 @@ type HostConnection = {
|
||||
|
||||
async function disableCodingEventSource(page: Page): Promise<void> {
|
||||
await page.addInitScript(() => {
|
||||
const sources = new Set<LocalEventSource>();
|
||||
type TrackedWindow = Window & {
|
||||
__makeloreCodingEventSources?: LocalEventSource[];
|
||||
};
|
||||
const trackedWindow = window as TrackedWindow;
|
||||
trackedWindow.__makeloreCodingEventSources = [];
|
||||
class LocalEventSource extends EventTarget {
|
||||
static readonly CONNECTING = 0;
|
||||
static readonly OPEN = 1;
|
||||
@@ -35,11 +41,14 @@ async function disableCodingEventSource(page: Page): Promise<void> {
|
||||
constructor(url: string) {
|
||||
super();
|
||||
this.url = url;
|
||||
sources.add(this);
|
||||
trackedWindow.__makeloreCodingEventSources?.push(this);
|
||||
queueMicrotask(() => this.onopen?.(new Event('open')));
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.readyState = LocalEventSource.CLOSED;
|
||||
sources.delete(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,9 +57,26 @@ async function disableCodingEventSource(page: Page): Promise<void> {
|
||||
writable: true,
|
||||
value: LocalEventSource,
|
||||
});
|
||||
Object.defineProperty(window, '__makeloreEmitCodingEvent', {
|
||||
configurable: true,
|
||||
value(type: string, payload: unknown) {
|
||||
for (const source of sources) {
|
||||
source.dispatchEvent(new MessageEvent(type, { data: JSON.stringify(payload) }));
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function emitCodingEvent(page: Page, type: string, payload: unknown): Promise<void> {
|
||||
await page.evaluate(({ eventType, eventPayload }) => {
|
||||
const testWindow = window as typeof window & {
|
||||
__makeloreEmitCodingEvent?: (type: string, payload: unknown) => void;
|
||||
};
|
||||
testWindow.__makeloreEmitCodingEvent?.(eventType, eventPayload);
|
||||
}, { eventType: type, eventPayload: payload });
|
||||
}
|
||||
|
||||
async function installCodingFirstChatHost(
|
||||
electronApp: ElectronApplication,
|
||||
hostConnection: HostConnection,
|
||||
@@ -66,6 +92,7 @@ async function installCodingFirstChatHost(
|
||||
abortRequested: boolean;
|
||||
releaseSnapshot: (() => void) | null;
|
||||
snapshotPending: boolean;
|
||||
snapshotSettled: boolean;
|
||||
};
|
||||
const mainGlobal = globalThis as typeof globalThis & {
|
||||
__makelorePiFirstChatE2E?: MainState;
|
||||
@@ -77,6 +104,7 @@ async function installCodingFirstChatHost(
|
||||
abortRequested: false,
|
||||
releaseSnapshot: null,
|
||||
snapshotPending: false,
|
||||
snapshotSettled: false,
|
||||
};
|
||||
mainGlobal.__makelorePiFirstChatE2E = state;
|
||||
const now = '2026-08-24T00:00:00.000Z';
|
||||
@@ -145,6 +173,12 @@ async function installCodingFirstChatHost(
|
||||
: null,
|
||||
modelResolution: featureComplete ? 'resolved' : 'required',
|
||||
};
|
||||
const historyConversation = {
|
||||
...conversation,
|
||||
id: 'conversation-pi-history',
|
||||
title: 'History and quota',
|
||||
updatedAt: '2026-08-23T23:58:00.000Z',
|
||||
};
|
||||
const snapshot = {
|
||||
schemaVersion: 1,
|
||||
conversation: {
|
||||
@@ -217,7 +251,7 @@ async function installCodingFirstChatHost(
|
||||
output: [{
|
||||
kind: 'text',
|
||||
id: 'browser-output-e2e',
|
||||
text: `${'Checking browser runtime state and the latest navigation checkpoint. '.repeat(5)}Browser ready`,
|
||||
text: `${'Checking browser runtime state and the latest navigation checkpoint. '.repeat(8)}Browser ready`,
|
||||
status: 'complete',
|
||||
}],
|
||||
details: { schema: 'agent-browser.v1', action: 'status' },
|
||||
@@ -355,6 +389,39 @@ async function installCodingFirstChatHost(
|
||||
error: { code: 'CODING_RUNTIME_START_FAILED', message: 'Worker stopped', recoverable: true },
|
||||
},
|
||||
};
|
||||
const historySnapshot = {
|
||||
...snapshot,
|
||||
conversation: {
|
||||
...snapshot.conversation,
|
||||
id: historyConversation.id,
|
||||
title: historyConversation.title,
|
||||
},
|
||||
nodes: [
|
||||
...Array.from({ length: 150 }, (_, index) => ({
|
||||
kind: 'message',
|
||||
id: `history-message-${index}`,
|
||||
role: 'user',
|
||||
status: 'complete',
|
||||
blocks: [{
|
||||
kind: 'text',
|
||||
id: `history-message-${index}:content:0`,
|
||||
text: `History message ${index}`,
|
||||
status: 'complete',
|
||||
}],
|
||||
})),
|
||||
{
|
||||
kind: 'notice',
|
||||
id: 'history-quota-notice',
|
||||
code: 'CODING_PROVIDER_QUOTA_EXHAUSTED',
|
||||
level: 'error',
|
||||
message: '词元点数余额不足,请充值后重试。',
|
||||
},
|
||||
],
|
||||
run: { status: 'idle' },
|
||||
queue: { items: [] },
|
||||
pendingInteractions: [],
|
||||
worker: { status: 'ready', generation: 1 },
|
||||
};
|
||||
const respond = (json: unknown, status = 200) => ({
|
||||
ok: true,
|
||||
data: { status, ok: status >= 200 && status < 300, json },
|
||||
@@ -455,7 +522,7 @@ async function installCodingFirstChatHost(
|
||||
if (path === `/api/coding/projects/conversations?projectId=${project.id}`) {
|
||||
return respond({
|
||||
conversations: featureComplete
|
||||
? [conversation, secondConversation]
|
||||
? [conversation, secondConversation, historyConversation]
|
||||
: state.conversationCreated
|
||||
? [conversation]
|
||||
: [],
|
||||
@@ -471,11 +538,28 @@ async function installCodingFirstChatHost(
|
||||
await new Promise<void>((resolve) => { state.releaseSnapshot = resolve; });
|
||||
state.snapshotPending = false;
|
||||
}
|
||||
const currentSnapshot = state.snapshotSettled
|
||||
? {
|
||||
...snapshot,
|
||||
run: {
|
||||
status: 'idle',
|
||||
runId: 'run-e2e-feature',
|
||||
mode: 'prompt',
|
||||
startedAt: 1_000,
|
||||
settledAt: 2_000,
|
||||
terminalReason: 'aborted',
|
||||
},
|
||||
queue: { items: [] },
|
||||
pendingInteractions: [],
|
||||
worker: { status: 'stopped', generation: 1 },
|
||||
cursor: { workerGeneration: 1, seq: 1 },
|
||||
}
|
||||
: snapshot;
|
||||
return respond({
|
||||
snapshot: state.abortRequested
|
||||
? {
|
||||
...snapshot,
|
||||
nodes: snapshot.nodes.map((node) => {
|
||||
...currentSnapshot,
|
||||
nodes: currentSnapshot.nodes.map((node) => {
|
||||
if (node.kind === 'message' && node.role === 'assistant') {
|
||||
return {
|
||||
...node,
|
||||
@@ -508,13 +592,16 @@ async function installCodingFirstChatHost(
|
||||
cursor: { workerGeneration: 1, seq: 1 },
|
||||
}
|
||||
: state.interactionAnswered
|
||||
? { ...snapshot, pendingInteractions: [] }
|
||||
: snapshot,
|
||||
? { ...currentSnapshot, pendingInteractions: [] }
|
||||
: currentSnapshot,
|
||||
});
|
||||
}
|
||||
if (path === `/api/coding/conversations/${secondConversation.id}/snapshot`) {
|
||||
return respond({ snapshot: secondSnapshot });
|
||||
}
|
||||
if (path === `/api/coding/conversations/${historyConversation.id}/snapshot`) {
|
||||
return respond({ snapshot: historySnapshot });
|
||||
}
|
||||
if (path === `/api/coding/conversations/${conversation.id}/prompt` && method === 'POST') {
|
||||
return respond({
|
||||
acceptance: {
|
||||
@@ -590,6 +677,17 @@ async function releaseSnapshot(electronApp: ElectronApplication): Promise<void>
|
||||
});
|
||||
}
|
||||
|
||||
async function settleSnapshot(electronApp: ElectronApplication): Promise<void> {
|
||||
await electronApp.evaluate(() => {
|
||||
const mainGlobal = globalThis as typeof globalThis & {
|
||||
__makelorePiFirstChatE2E?: { snapshotSettled: boolean };
|
||||
};
|
||||
if (mainGlobal.__makelorePiFirstChatE2E) {
|
||||
mainGlobal.__makelorePiFirstChatE2E.snapshotSettled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
test('first PI Conversation is editable under 500 ms and submits before runtime Snapshot', async ({
|
||||
launchElectronApp,
|
||||
}) => {
|
||||
@@ -684,6 +782,217 @@ test('first PI Conversation is editable under 500 ms and submits before runtime
|
||||
}
|
||||
});
|
||||
|
||||
test('hidden Conversation badge ignores process failures until interaction or task settlement', async ({
|
||||
launchElectronApp,
|
||||
}) => {
|
||||
const electronApp = await launchElectronApp({ skipSetup: true });
|
||||
let page = await getStableWindow(electronApp);
|
||||
const hostConnection = await page.evaluate(async () => ({
|
||||
token: await window.electron.ipcRenderer.invoke('hostapi:token') as string,
|
||||
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
|
||||
}));
|
||||
await installCodingFirstChatHost(electronApp, hostConnection, true);
|
||||
await disableCodingEventSource(page);
|
||||
|
||||
try {
|
||||
await page.reload();
|
||||
page = await getStableWindow(electronApp);
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
await page.evaluate(() => { window.location.hash = '/chat'; });
|
||||
|
||||
const conversations = page.getByRole('group', { name: 'Builder 的对话' });
|
||||
const firstConversation = conversations.getByRole('button', { name: '新对话', exact: true });
|
||||
const secondConversation = conversations.getByRole('button', { name: 'Second Conversation' });
|
||||
const secondUnreadBadge = secondConversation.locator('[aria-label="未读"]');
|
||||
await expect(page.getByTestId('coding-conversation-header')).toContainText('新对话');
|
||||
await expect(secondUnreadBadge).toHaveCount(0);
|
||||
|
||||
await emitCodingEvent(page, 'patch-batch', {
|
||||
type: 'patch-batch',
|
||||
conversationId: 'conversation-pi-second',
|
||||
workerGeneration: 1,
|
||||
fromSeq: 1,
|
||||
toSeq: 3,
|
||||
items: [
|
||||
{
|
||||
seq: 1,
|
||||
at: 1_001,
|
||||
runId: 'run-process-e2e',
|
||||
patch: {
|
||||
op: 'run.state',
|
||||
run: { status: 'running', runId: 'run-process-e2e', mode: 'prompt', startedAt: 1_001 },
|
||||
},
|
||||
},
|
||||
{
|
||||
seq: 2,
|
||||
at: 1_002,
|
||||
runId: 'run-process-e2e',
|
||||
patch: {
|
||||
op: 'message.upsert',
|
||||
node: {
|
||||
kind: 'message',
|
||||
id: 'message-process-e2e',
|
||||
role: 'assistant',
|
||||
status: 'streaming',
|
||||
blocks: [{ kind: 'thinking', id: 'thinking-process-e2e', text: 'Checking', status: 'streaming' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
seq: 3,
|
||||
at: 1_003,
|
||||
runId: 'run-process-e2e',
|
||||
patch: {
|
||||
op: 'tool.upsert',
|
||||
node: {
|
||||
kind: 'tool',
|
||||
id: 'tool-process-e2e',
|
||||
toolCallId: 'tool-call-process-e2e',
|
||||
toolName: 'bash',
|
||||
title: 'Run command',
|
||||
inputText: 'false',
|
||||
status: 'error',
|
||||
output: [{ kind: 'text', id: 'tool-output-process-e2e', text: 'Command failed', status: 'complete' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
await expect(secondUnreadBadge).toHaveCount(0);
|
||||
|
||||
await emitCodingEvent(page, 'patch-batch', {
|
||||
type: 'patch-batch',
|
||||
conversationId: 'conversation-pi-second',
|
||||
workerGeneration: 1,
|
||||
fromSeq: 4,
|
||||
toSeq: 4,
|
||||
items: [{
|
||||
seq: 4,
|
||||
at: 1_004,
|
||||
runId: 'run-process-e2e',
|
||||
patch: {
|
||||
op: 'interaction.upsert',
|
||||
interaction: {
|
||||
id: 'interaction-process-e2e',
|
||||
conversationId: 'conversation-pi-second',
|
||||
runId: 'run-process-e2e',
|
||||
kind: 'confirm',
|
||||
title: 'Allow this action?',
|
||||
status: 'pending',
|
||||
},
|
||||
},
|
||||
}],
|
||||
});
|
||||
await expect(secondUnreadBadge).toHaveCount(1);
|
||||
|
||||
await secondConversation.click();
|
||||
await expect(page.getByTestId('coding-conversation-header')).toContainText('Second Conversation');
|
||||
await expect(secondUnreadBadge).toHaveCount(0);
|
||||
await firstConversation.click();
|
||||
await expect(page.getByTestId('coding-conversation-header')).toContainText('新对话');
|
||||
|
||||
await emitCodingEvent(page, 'patch-batch', {
|
||||
type: 'patch-batch',
|
||||
conversationId: 'conversation-pi-second',
|
||||
workerGeneration: 1,
|
||||
fromSeq: 5,
|
||||
toSeq: 5,
|
||||
items: [{
|
||||
seq: 5,
|
||||
at: 1_005,
|
||||
runId: 'run-process-e2e',
|
||||
patch: {
|
||||
op: 'interaction.remove',
|
||||
interactionId: 'interaction-process-e2e',
|
||||
},
|
||||
}],
|
||||
});
|
||||
await expect(secondUnreadBadge).toHaveCount(0);
|
||||
|
||||
await emitCodingEvent(page, 'patch-batch', {
|
||||
type: 'patch-batch',
|
||||
conversationId: 'conversation-pi-second',
|
||||
workerGeneration: 1,
|
||||
fromSeq: 6,
|
||||
toSeq: 6,
|
||||
items: [{
|
||||
seq: 6,
|
||||
at: 1_006,
|
||||
runId: 'run-process-e2e',
|
||||
patch: {
|
||||
op: 'run.state',
|
||||
run: {
|
||||
status: 'idle',
|
||||
runId: 'run-process-e2e',
|
||||
mode: 'prompt',
|
||||
startedAt: 1_001,
|
||||
settledAt: 1_006,
|
||||
terminalReason: 'completed',
|
||||
},
|
||||
},
|
||||
}],
|
||||
});
|
||||
await expect(secondUnreadBadge).toHaveCount(1);
|
||||
} finally {
|
||||
await releaseSnapshot(electronApp);
|
||||
}
|
||||
});
|
||||
|
||||
test('foreground focus rehydrates a terminal Snapshot after lifecycle sleep', async ({
|
||||
launchElectronApp,
|
||||
}) => {
|
||||
const electronApp = await launchElectronApp({ skipSetup: true });
|
||||
let page = await getStableWindow(electronApp);
|
||||
const hostConnection = await page.evaluate(async () => ({
|
||||
token: await window.electron.ipcRenderer.invoke('hostapi:token') as string,
|
||||
baseUrl: await window.electron.ipcRenderer.invoke('hostapi:base-url') as string,
|
||||
}));
|
||||
await installCodingFirstChatHost(electronApp, hostConnection, true);
|
||||
await disableCodingEventSource(page);
|
||||
|
||||
await page.reload();
|
||||
page = await getStableWindow(electronApp);
|
||||
await page.getByTestId('ai-module-option-programming').click();
|
||||
await expect(page.getByTestId('main-layout')).toBeVisible();
|
||||
await page.evaluate(() => { window.location.hash = '/chat'; });
|
||||
|
||||
const processGroup = page.getByTestId('coding-process-group');
|
||||
await expect(processGroup).toHaveAttribute('data-process-state', 'active');
|
||||
await expect(processGroup.locator('summary').first()).toContainText('处理中');
|
||||
const snapshotPath = '/api/coding/conversations/conversation-pi-first-chat/snapshot';
|
||||
const stateBeforeSleep = await readState(electronApp);
|
||||
const snapshotCallsBeforeSleep = stateBeforeSleep.captured.filter((request) => (
|
||||
request.path === snapshotPath
|
||||
)).length;
|
||||
await settleSnapshot(electronApp);
|
||||
|
||||
await electronApp.evaluate(({ BrowserWindow }) => {
|
||||
for (const window of BrowserWindow.getAllWindows()) {
|
||||
window.webContents.send('lifecycle:sleep');
|
||||
}
|
||||
});
|
||||
await expect.poll(async () => page.evaluate(() => {
|
||||
const trackedWindow = window as typeof window & {
|
||||
__makeloreCodingEventSources?: Array<{ readyState: number }>;
|
||||
};
|
||||
return trackedWindow.__makeloreCodingEventSources?.at(-1)?.readyState ?? -1;
|
||||
})).toBe(2);
|
||||
|
||||
await page.evaluate(() => window.dispatchEvent(new FocusEvent('focus')));
|
||||
|
||||
await expect(processGroup).toHaveAttribute('data-process-state', 'settled');
|
||||
await expect(processGroup.locator('summary').first()).not.toContainText('处理中');
|
||||
await expect.poll(async () => {
|
||||
const state = await readState(electronApp);
|
||||
return state.captured.filter((request) => request.path === snapshotPath).length;
|
||||
}).toBeGreaterThan(snapshotCallsBeforeSleep);
|
||||
const stateAfterFocus = await readState(electronApp);
|
||||
expect(stateAfterFocus.captured.some((request) => (
|
||||
request.path === '/api/coding/conversations/conversation-pi-first-chat/prompt'
|
||||
))).toBe(false);
|
||||
});
|
||||
|
||||
test('PI feature UI isolates Conversations and exposes queue, interaction, model, and subagent state', async ({
|
||||
launchElectronApp,
|
||||
}) => {
|
||||
@@ -736,13 +1045,13 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
||||
await expect(activeProcess).toHaveAttribute('open', '');
|
||||
await expect(activeProcess.locator('summary').first()).toContainText('处理中');
|
||||
const activeThinking = page.getByLabel('思考过程');
|
||||
await expect(activeThinking).toContainText('Preparing the smallest safe next step.');
|
||||
await expect(activeThinking).not.toContainText('Inspecting the project before responding.');
|
||||
await expect(activeThinking).toContainText('Inspecting the project before responding.');
|
||||
await expect(activeThinking).not.toContainText('Preparing the smallest safe next step.');
|
||||
await expect(activeThinking).toHaveAttribute('data-collapsed-lines', '1');
|
||||
await expect(activeThinking).toHaveAttribute('data-expanded', 'false');
|
||||
await expect(activeThinking).toHaveAttribute('data-streaming', 'true');
|
||||
const activeThinkingPreview = activeThinking.getByTestId('process-progress-preview');
|
||||
await expect(activeThinkingPreview).toHaveAttribute('data-progress-tail', 'true');
|
||||
await expect(activeThinkingPreview).toHaveAttribute('data-progress-origin', 'head');
|
||||
await expect(activeThinkingPreview).toHaveAttribute('data-progress-alignment', 'left');
|
||||
await expect(activeThinkingPreview).toHaveAttribute('data-progress-update-motion', 'none');
|
||||
await expect(activeThinkingPreview).not.toHaveAttribute('data-roll-revision');
|
||||
@@ -774,6 +1083,7 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
||||
await expect(activeCommentary).toHaveClass(/text-foreground/);
|
||||
const activeCommentaryPreview = activeCommentary.getByTestId('process-progress-preview');
|
||||
await expect(activeCommentaryPreview).toContainText('Durable assistant response');
|
||||
await expect(activeCommentaryPreview).toHaveAttribute('data-progress-origin', 'head');
|
||||
await expect(activeCommentaryPreview).toHaveClass(/text-foreground/);
|
||||
await expect(activeCommentaryPreview).not.toHaveClass(/text-muted-foreground/);
|
||||
await expect(activeCommentaryPreview).toHaveAttribute('data-progress-shimmer', 'false');
|
||||
@@ -792,9 +1102,11 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
||||
const compactToolSummary = compactTool.locator('> summary');
|
||||
await expect(compactTool).not.toHaveAttribute('open', '');
|
||||
await expect(compactToolSummary).toHaveAttribute('data-collapsed-lines', '1');
|
||||
await expect(compactTool.getByTestId('tool-progress-preview')).toContainText('Browser ready');
|
||||
await expect(compactTool.getByTestId('tool-progress-preview'))
|
||||
.toHaveAttribute('data-progress-tail', 'true');
|
||||
.toContainText('Checking browser runtime state');
|
||||
await expect(compactTool.getByTestId('tool-progress-preview')).not.toContainText('Browser ready');
|
||||
await expect(compactTool.getByTestId('tool-progress-preview'))
|
||||
.toHaveAttribute('data-progress-origin', 'head');
|
||||
const toolProgressViewport = compactTool.getByTestId('tool-progress-preview');
|
||||
await expect(toolProgressViewport).toHaveAttribute('data-progress-alignment', 'left');
|
||||
await expect(toolProgressViewport).toHaveAttribute('data-progress-shimmer', 'false');
|
||||
@@ -806,10 +1118,7 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
||||
scrollWidth: element.scrollWidth,
|
||||
}));
|
||||
expect(toolProgressScroll.scrollWidth).toBeGreaterThan(toolProgressScroll.clientWidth);
|
||||
expect(Math.abs(
|
||||
toolProgressScroll.scrollLeft
|
||||
- (toolProgressScroll.scrollWidth - toolProgressScroll.clientWidth),
|
||||
)).toBeLessThanOrEqual(2);
|
||||
expect(Math.abs(toolProgressScroll.scrollLeft)).toBeLessThanOrEqual(2);
|
||||
const compactToolBounds = await compactToolSummary.boundingBox();
|
||||
expect(compactToolBounds).not.toBeNull();
|
||||
expect(compactToolBounds!.height).toBeLessThanOrEqual(33);
|
||||
@@ -941,6 +1250,29 @@ test('PI feature UI isolates Conversations and exposes queue, interaction, model
|
||||
await expect(page.getByRole('textbox')).toHaveValue('');
|
||||
await page.getByRole('button', { name: '恢复' }).click();
|
||||
|
||||
await builderConversations.getByRole('button', { name: 'History and quota' }).click();
|
||||
const historyProcess = page.getByTestId('coding-process-group');
|
||||
await expect(historyProcess.locator('summary').first())
|
||||
.toContainText('词元点数余额不足,请充值后重试。');
|
||||
await expect(page.getByText('History message 0')).toHaveCount(0);
|
||||
await expect(page.getByText('History message 31')).toHaveCount(1);
|
||||
const historyTimeline = page.getByTestId('coding-conversation-timeline');
|
||||
const beforeHeight = await historyTimeline.evaluate((element) => {
|
||||
element.scrollTop = 0;
|
||||
const height = element.scrollHeight;
|
||||
element.dispatchEvent(new Event('scroll', { bubbles: true }));
|
||||
return height;
|
||||
});
|
||||
await expect(page.getByText('History message 0')).toHaveCount(1);
|
||||
const anchoredScroll = await historyTimeline.evaluate((element) => ({
|
||||
scrollHeight: element.scrollHeight,
|
||||
scrollTop: element.scrollTop,
|
||||
}));
|
||||
expect(anchoredScroll.scrollTop).toBeGreaterThan(0);
|
||||
expect(Math.abs(
|
||||
anchoredScroll.scrollTop - (anchoredScroll.scrollHeight - beforeHeight),
|
||||
)).toBeLessThanOrEqual(2);
|
||||
|
||||
await expect(page.getByText(/编程工具|分享|取消分享|回滚|恢复回滚|待办|全局运行时/)).toHaveCount(0);
|
||||
const state = await readState(electronApp);
|
||||
expect(state.captured.some((request) => request.path.endsWith('/model') && request.method === 'POST')).toBe(true);
|
||||
|
||||
Reference in New Issue
Block a user