* perf(app): keep composer typing responsive The controlled web textarea pushed every keystroke through the draft store and broad composer tree before paint. Keep live text in the focused DOM input and publish draft and selection state after paint so persistence and reactive features remain intact without blocking the typed character. * fix(app): preserve live composer state at actions * fix(app): use live input for autocomplete clicks * fix(app): keep submit activity atomic
137 lines
4.7 KiB
TypeScript
137 lines
4.7 KiB
TypeScript
import { expect, test } from "../support/fixtures";
|
|
import { gotoAppShell } from "../support/helpers/app";
|
|
import { getE2EDaemonPort } from "../support/helpers/daemon-port";
|
|
import {
|
|
expectNewWorkspaceDraft,
|
|
expectNewWorkspaceProjectSelected,
|
|
fillNewWorkspaceDraft,
|
|
openGlobalNewWorkspaceComposer,
|
|
openNewWorkspaceComposer,
|
|
selectNewWorkspaceHost,
|
|
selectNewWorkspaceProject,
|
|
} from "../support/helpers/new-workspace";
|
|
import { seedWorkspace, type SeededWorkspace } from "../support/helpers/seed-client";
|
|
import { getServerId } from "../support/helpers/server-id";
|
|
import { seedSavedSettingsHosts } from "../support/helpers/settings";
|
|
import { waitForSidebarHydration } from "../support/helpers/workspace-ui";
|
|
|
|
const DRAFT = `Please investigate the workspace startup failure.
|
|
|
|
Trace the request from the app through the daemon, preserve the existing behavior, and explain the root cause before making changes.`;
|
|
|
|
test.describe("New workspace composer draft", () => {
|
|
test.describe.configure({ timeout: 240_000 });
|
|
|
|
test("keeps the draft when the project changes", async ({ page }) => {
|
|
const firstProject: SeededWorkspace = await seedWorkspace({
|
|
repoPrefix: "new-workspace-draft-project-a-",
|
|
});
|
|
const secondProject: SeededWorkspace = await seedWorkspace({
|
|
repoPrefix: "new-workspace-draft-project-b-",
|
|
});
|
|
|
|
try {
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await openNewWorkspaceComposer(page, {
|
|
projectKey: firstProject.projectKey,
|
|
projectDisplayName: firstProject.projectDisplayName,
|
|
});
|
|
await expectNewWorkspaceProjectSelected(page, firstProject.projectDisplayName);
|
|
|
|
await fillNewWorkspaceDraft(page, DRAFT);
|
|
|
|
await selectNewWorkspaceProject(page, {
|
|
projectKey: secondProject.projectKey,
|
|
projectDisplayName: secondProject.projectDisplayName,
|
|
});
|
|
|
|
await expectNewWorkspaceDraft(page, DRAFT);
|
|
} finally {
|
|
await secondProject.cleanup();
|
|
await firstProject.cleanup();
|
|
}
|
|
});
|
|
|
|
test("keeps the draft when the host changes", async ({ page }) => {
|
|
const project: SeededWorkspace = await seedWorkspace({
|
|
repoPrefix: "new-workspace-draft-host-",
|
|
});
|
|
const secondaryServerId = "new-workspace-draft-secondary-host";
|
|
|
|
try {
|
|
await seedSavedSettingsHosts(page, [
|
|
{
|
|
serverId: getServerId(),
|
|
label: "Primary host",
|
|
endpoint: `127.0.0.1:${getE2EDaemonPort()}`,
|
|
},
|
|
{
|
|
serverId: secondaryServerId,
|
|
label: "Secondary host",
|
|
endpoint: "127.0.0.1:9",
|
|
},
|
|
]);
|
|
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await openGlobalNewWorkspaceComposer(page);
|
|
|
|
await fillNewWorkspaceDraft(page, DRAFT);
|
|
await selectNewWorkspaceHost(page, "Secondary host");
|
|
|
|
await expectNewWorkspaceDraft(page, DRAFT);
|
|
} finally {
|
|
await project.cleanup();
|
|
}
|
|
});
|
|
|
|
test("does not restore a submitted draft after deferred publication", async ({ page }) => {
|
|
const project = await seedWorkspace({ repoPrefix: "new-workspace-submitted-draft-" });
|
|
|
|
try {
|
|
await gotoAppShell(page);
|
|
await waitForSidebarHydration(page);
|
|
await openNewWorkspaceComposer(page, {
|
|
projectKey: project.projectKey,
|
|
projectDisplayName: project.projectDisplayName,
|
|
});
|
|
|
|
const composer = page.getByRole("textbox", { name: "Message agent..." });
|
|
const createButton = page.getByTestId("workspace-create-submit");
|
|
await expect(composer).toBeEditable({ timeout: 30_000 });
|
|
await expect(createButton).toBeEnabled({ timeout: 30_000 });
|
|
|
|
await composer.evaluate((element, draft) => {
|
|
if (!(element instanceof HTMLTextAreaElement)) {
|
|
throw new Error("Composer input is not a textarea");
|
|
}
|
|
const valueSetter = Object.getOwnPropertyDescriptor(
|
|
HTMLTextAreaElement.prototype,
|
|
"value",
|
|
)?.set;
|
|
if (!valueSetter) throw new Error("Textarea value setter is unavailable");
|
|
valueSetter.call(element, draft);
|
|
element.dispatchEvent(
|
|
new InputEvent("input", {
|
|
bubbles: true,
|
|
data: draft,
|
|
inputType: "insertText",
|
|
}),
|
|
);
|
|
const button = document.querySelector('[data-testid="workspace-create-submit"]');
|
|
if (!(button instanceof HTMLElement)) {
|
|
throw new Error("Create button is unavailable");
|
|
}
|
|
button.click();
|
|
}, DRAFT);
|
|
|
|
await page.waitForURL((url) => url.pathname.includes("/workspace/"), { timeout: 30_000 });
|
|
await openGlobalNewWorkspaceComposer(page);
|
|
await expectNewWorkspaceDraft(page, "");
|
|
} finally {
|
|
await project.cleanup();
|
|
}
|
|
});
|
|
});
|