Add aligned assistant timestamps and Copy/Fork actions
This commit is contained in:
@@ -28,7 +28,8 @@ const chat = { id: "mobile-chat", title: "Mobile QA conversation with a long tit
|
||||
lastUsedProvider: "openai", lastUsedModel: "mobile-model-one", additionalSystemPrompt: null, enabledTools: [],
|
||||
messages: [{ id: "mobile-assistant", role: "assistant", content: markdown, createdAt: date, name: null, metadata: null }] };
|
||||
const idle = { state: "idle", isThinking: false, queued: 0, backgroundWorkers: 0, tools: [], queuedMessages: [] };
|
||||
const errors = [], calls = [];
|
||||
const errors = [], calls = [], forkRequests = [];
|
||||
let forkedChat;
|
||||
let readCursor = "0";
|
||||
await context.addInitScript(() => localStorage.setItem("sybil_admin_token", "mobile-fixture-token"));
|
||||
await context.route("**/*", async route => {
|
||||
@@ -39,14 +40,20 @@ await context.route("**/*", async route => {
|
||||
calls.push({ path, method });
|
||||
let value;
|
||||
if (path.endsWith("/auth/session")) value = { mode: "token", authenticated: true };
|
||||
else if (path.endsWith("/workspace-items")) value = { items: [{ ...chat, type: "chat" }] };
|
||||
else if (path.endsWith("/workspace-items")) value = { items: [chat, ...(forkedChat ? [forkedChat] : [])].map(chat => ({ ...chat, type: "chat" })) };
|
||||
else if (path.endsWith("/models")) value = { providers: { openai: { models: ["mobile-model-one", "mobile-model-two"], loadedAt: date, error: null } } };
|
||||
else if (path.endsWith("/chat-tools") || path.endsWith("/agent/tools")) value = { tools: [] };
|
||||
else if (path.endsWith("/active-runs")) value = { chats: [], searches: [] };
|
||||
else if (path.endsWith("/chats/mobile-chat")) {
|
||||
if (method === "PATCH") Object.assign(chat, route.request().postDataJSON());
|
||||
value = { chat };
|
||||
} else if (path.endsWith("/agent/messages")) value = { conversationId: "1", messages: [{ ...chat.messages[0], id: "outbound:1", metadata: { entryId: 1 } }], cursor: "1", beforeCursor: null, hasMore: false };
|
||||
} else if (path.endsWith("/chats/mobile-chat/fork")) {
|
||||
forkRequests.push(route.request().postDataJSON());
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
forkedChat = { ...chat, id: "mobile-fork", title: "Forked conversation", parentChatId: chat.id };
|
||||
value = { chat: forkedChat };
|
||||
} else if (path.endsWith("/chats/mobile-fork")) value = { chat: forkedChat };
|
||||
else if (path.endsWith("/agent/messages")) value = { conversationId: "1", messages: [{ ...chat.messages[0], id: "outbound:1", metadata: { entryId: 1 } }], cursor: "1", beforeCursor: null, hasMore: false };
|
||||
else if (path.endsWith("/agent")) value = { id: "1", title: "Sybil Agent", activity: idle, protocol: "pi-room.v1", websocket: "/v1/agent/ws" };
|
||||
else if (path.endsWith("/read-state")) {
|
||||
if (method === "PUT") readCursor = route.request().postDataJSON().cursor;
|
||||
@@ -95,6 +102,24 @@ async function scrollHorizontally(locator) {
|
||||
async function contextMenuCanceled(locator) {
|
||||
return locator.evaluate(el => !el.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: 100, clientY: 200 })));
|
||||
}
|
||||
async function checkFooterAlignment() {
|
||||
const footer = page.locator('[data-assistant-footer="mobile-assistant"]');
|
||||
await footer.scrollIntoViewIfNeeded();
|
||||
// Leave space for the fixed composer when inspecting the final reply actions.
|
||||
await footer.evaluate(el => el.scrollIntoView({ block: "center" }));
|
||||
const layout = await footer.evaluate(el => ({
|
||||
paragraphLeft: el.parentElement.querySelector(".md-content > p").getBoundingClientRect().left,
|
||||
dateLeft: el.querySelector("time").getBoundingClientRect().left,
|
||||
actionLeft: el.querySelector("button svg").getBoundingClientRect().left,
|
||||
dateColor: getComputedStyle(el.querySelector("time")).color,
|
||||
paragraphColor: getComputedStyle(el.parentElement.querySelector(".md-content")).color,
|
||||
}));
|
||||
assert.ok(Math.abs(layout.dateLeft - layout.paragraphLeft) < 1, "timestamp aligns with reply text");
|
||||
assert.ok(Math.abs(layout.actionLeft - layout.paragraphLeft) < 1, "first action icon aligns with reply text");
|
||||
assert.notEqual(layout.dateColor, layout.paragraphColor, "timestamp uses secondary color");
|
||||
assert.equal(await footer.locator("time").getAttribute("datetime"), date);
|
||||
assert.match(await footer.locator("time").textContent(), /2026/);
|
||||
}
|
||||
try {
|
||||
await page.goto(target.href, { waitUntil: "networkidle" });
|
||||
await selectChat();
|
||||
@@ -173,10 +198,24 @@ try {
|
||||
await composer.focus(); await page.keyboard.press("Control+v");
|
||||
assert.equal(await composer.inputValue(), "native pasted draft");
|
||||
assert.equal(calls.filter(call => call.method === "POST").length, 0);
|
||||
// Explicit message actions preserve Fork without replacing the native menu.
|
||||
await page.getByRole("button", { name: "Message actions", exact: true }).click();
|
||||
await page.getByRole("button", { name: "Fork", exact: true }).waitFor();
|
||||
await page.keyboard.press("Escape");
|
||||
// The footer uses the saved date and exposes Copy/Fork without a custom menu.
|
||||
await checkFooterAlignment();
|
||||
const copy = page.getByRole("button", { name: "Copy message", exact: true });
|
||||
await copy.click();
|
||||
await page.getByText("Message copied.", { exact: true }).waitFor({ state: "attached" });
|
||||
assert.equal(await page.evaluate(() => navigator.clipboard.readText()), markdown);
|
||||
await page.evaluate(() => Object.defineProperty(navigator.clipboard, "writeText", { configurable: true, value: async () => { throw new DOMException("Denied", "NotAllowedError"); } }));
|
||||
await copy.click();
|
||||
await page.getByText(/Could not copy\. Select the message text/).waitFor();
|
||||
await page.evaluate(() => delete navigator.clipboard.writeText);
|
||||
await copy.focus(); await page.keyboard.press("Enter");
|
||||
await page.getByText("Message copied.", { exact: true }).waitFor({ state: "attached" });
|
||||
const fork = page.getByRole("button", { name: "Fork chat from this message", exact: true });
|
||||
await fork.click();
|
||||
assert.equal(await fork.isDisabled(), true);
|
||||
await page.getByRole("heading", { name: "Forked conversation", exact: true }).waitFor();
|
||||
assert.deepEqual(forkRequests, [{ messageId: "mobile-assistant" }]);
|
||||
await selectChat();
|
||||
await page.getByRole("button", { name: "Open chat settings" }).click();
|
||||
await page.getByRole("dialog").waitFor();
|
||||
await page.getByPlaceholder("Select or type model", { exact: true }).fill("mobile-model-two");
|
||||
@@ -188,6 +227,7 @@ try {
|
||||
await page.setViewportSize({ width, height: 844 });
|
||||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false, `${width}px page overflow`);
|
||||
assert.ok(await page.locator("main header p").isVisible());
|
||||
await checkFooterAlignment();
|
||||
if (artifactDir) await page.screenshot({ path: `${artifactDir}/regular-${width}.png` });
|
||||
}
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
@@ -212,15 +252,20 @@ try {
|
||||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
|
||||
assert.ok(await page.locator("main header button").filter({ hasText: "mobile-model-one" }).isVisible());
|
||||
assert.equal(await contextMenuCanceled(paragraph), false);
|
||||
await checkFooterAlignment();
|
||||
await copy.click();
|
||||
assert.equal(await page.evaluate(() => navigator.clipboard.readText()), markdown);
|
||||
if (artifactDir) await page.screenshot({ path: `${artifactDir}/regular-1280.png` });
|
||||
const manifest = await page.evaluate(async () => (await fetch("/manifest.webmanifest")).json());
|
||||
assert.equal(manifest.display, "standalone"); assert.ok(!manifest.display_override?.includes("fullscreen"));
|
||||
const viewport = await page.locator('meta[name="viewport"]').getAttribute("content");
|
||||
assert.match(viewport, /user-scalable=no/);
|
||||
assert.match(viewport, /maximum-scale=1\.0(?:,|$)/);
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.waitForFunction(() => document.querySelector(".workspace-sidebar").getBoundingClientRect().right <= 1);
|
||||
await drag(300, 8, -160);
|
||||
await page.getByRole("heading", { name: "New chat", exact: true }).waitFor();
|
||||
assert.deepEqual(errors, []);
|
||||
console.log("PASS mobile horizontal scrolling/edges, native menu/copy/paste, selection/multitouch/cancellation, compact widths/models, Agent layout, shorter viewport, desktop and PWA settings");
|
||||
console.log("PASS mobile horizontal scrolling/edges, native menu/copy/paste, selection/multitouch/cancellation, compact widths/models, aligned timestamp and Copy/Fork footer, Agent layout, shorter viewport, desktop and PWA settings");
|
||||
}
|
||||
} finally { await context.close(); await browser.close(); }
|
||||
|
||||
Reference in New Issue
Block a user