-
Notifications
You must be signed in to change notification settings - Fork 32
🤖 ci: add robust Electron E2E tests for regression prevention #884
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| import type { ScenarioTurn } from "@/node/services/mock/scenarioTypes"; | ||
| import { STREAM_BASE_DELAY } from "@/node/services/mock/scenarioTypes"; | ||
| import { KNOWN_MODELS } from "@/common/constants/knownModels"; | ||
|
|
||
| export const ERROR_PROMPTS = { | ||
| TRIGGER_RATE_LIMIT: "Trigger rate limit error", | ||
| TRIGGER_API_ERROR: "Trigger API error", | ||
| TRIGGER_NETWORK_ERROR: "Trigger network error", | ||
| } as const; | ||
|
|
||
| export const ERROR_MESSAGES = { | ||
| RATE_LIMIT: "Rate limit exceeded. Please retry after 60 seconds.", | ||
| API_ERROR: "Internal server error occurred while processing the request.", | ||
| NETWORK_ERROR: "Network connection lost. Please check your internet connection.", | ||
| } as const; | ||
|
|
||
| const rateLimitErrorTurn: ScenarioTurn = { | ||
| user: { | ||
| text: ERROR_PROMPTS.TRIGGER_RATE_LIMIT, | ||
| thinkingLevel: "low", | ||
| mode: "exec", | ||
| }, | ||
| assistant: { | ||
| messageId: "msg-error-ratelimit", | ||
| events: [ | ||
| { | ||
| kind: "stream-start", | ||
| delay: 0, | ||
| messageId: "msg-error-ratelimit", | ||
| model: KNOWN_MODELS.GPT.id, | ||
| }, | ||
| { | ||
| kind: "stream-delta", | ||
| delay: STREAM_BASE_DELAY, | ||
| text: "Processing your request...", | ||
| }, | ||
| { | ||
| kind: "stream-error", | ||
| delay: STREAM_BASE_DELAY * 2, | ||
| error: ERROR_MESSAGES.RATE_LIMIT, | ||
| errorType: "rate_limit", | ||
| }, | ||
| ], | ||
| }, | ||
| }; | ||
|
|
||
| const apiErrorTurn: ScenarioTurn = { | ||
| user: { | ||
| text: ERROR_PROMPTS.TRIGGER_API_ERROR, | ||
| thinkingLevel: "low", | ||
| mode: "exec", | ||
| }, | ||
| assistant: { | ||
| messageId: "msg-error-api", | ||
| events: [ | ||
| { | ||
| kind: "stream-start", | ||
| delay: 0, | ||
| messageId: "msg-error-api", | ||
| model: KNOWN_MODELS.GPT.id, | ||
| }, | ||
| { | ||
| kind: "stream-error", | ||
| delay: STREAM_BASE_DELAY, | ||
| error: ERROR_MESSAGES.API_ERROR, | ||
| errorType: "server_error", | ||
| }, | ||
| ], | ||
| }, | ||
| }; | ||
|
|
||
| const networkErrorTurn: ScenarioTurn = { | ||
| user: { | ||
| text: ERROR_PROMPTS.TRIGGER_NETWORK_ERROR, | ||
| thinkingLevel: "low", | ||
| mode: "exec", | ||
| }, | ||
| assistant: { | ||
| messageId: "msg-error-network", | ||
| events: [ | ||
| { | ||
| kind: "stream-start", | ||
| delay: 0, | ||
| messageId: "msg-error-network", | ||
| model: KNOWN_MODELS.GPT.id, | ||
| }, | ||
| { | ||
| kind: "stream-error", | ||
| delay: STREAM_BASE_DELAY, | ||
| error: ERROR_MESSAGES.NETWORK_ERROR, | ||
| errorType: "network", | ||
| }, | ||
| ], | ||
| }, | ||
| }; | ||
|
|
||
| export const scenarios: ScenarioTurn[] = [rateLimitErrorTurn, apiErrorTurn, networkErrorTurn]; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import { electronTest as test, electronExpect as expect } from "../electronTest"; | ||
| import { LIST_PROGRAMMING_LANGUAGES } from "@/node/services/mock/scenarios/basicChat"; | ||
|
|
||
| test.skip( | ||
| ({ browserName }) => browserName !== "chromium", | ||
| "Electron scenario runs on chromium only" | ||
| ); | ||
|
|
||
| test.describe("persistence", () => { | ||
| test("chat history persists across page reload", async ({ ui, page }) => { | ||
| await ui.projects.openFirstWorkspace(); | ||
|
|
||
| await ui.chat.captureStreamTimeline(async () => { | ||
| await ui.chat.sendMessage(LIST_PROGRAMMING_LANGUAGES); | ||
| }); | ||
| await ui.chat.expectTranscriptContains("Python"); | ||
|
|
||
| await page.reload(); | ||
| await page.waitForLoadState("domcontentloaded"); | ||
| await ui.projects.openFirstWorkspace(); | ||
|
|
||
| await ui.chat.expectTranscriptContains("Python"); | ||
| }); | ||
|
|
||
| test("chat history survives settings navigation", async ({ ui }) => { | ||
| await ui.projects.openFirstWorkspace(); | ||
|
|
||
| await ui.chat.captureStreamTimeline(async () => { | ||
| await ui.chat.sendMessage(LIST_PROGRAMMING_LANGUAGES); | ||
| }); | ||
|
|
||
| // Navigate through settings (potential state corruption points) | ||
| await ui.settings.open(); | ||
| await ui.settings.selectSection("Models"); | ||
| await ui.settings.selectSection("Providers"); | ||
| await ui.settings.close(); | ||
|
|
||
| await ui.chat.expectTranscriptContains("Python"); | ||
| await ui.chat.expectTranscriptContains("JavaScript"); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| import { electronTest as test, electronExpect as expect } from "../electronTest"; | ||
| import { LIST_PROGRAMMING_LANGUAGES } from "@/node/services/mock/scenarios/basicChat"; | ||
| import { ERROR_PROMPTS, ERROR_MESSAGES } from "@/node/services/mock/scenarios/errorScenarios"; | ||
|
|
||
| test.skip( | ||
| ({ browserName }) => browserName !== "chromium", | ||
| "Electron scenario runs on chromium only" | ||
| ); | ||
|
|
||
| test.describe("streaming behavior", () => { | ||
| test("stream continues after settings modal opens", async ({ ui, page }) => { | ||
| await ui.projects.openFirstWorkspace(); | ||
|
|
||
| const streamPromise = ui.chat.captureStreamTimeline(async () => { | ||
| await ui.chat.sendMessage(LIST_PROGRAMMING_LANGUAGES); | ||
| }); | ||
|
|
||
| await page.waitForTimeout(50); | ||
| await ui.settings.open(); | ||
| const timeline = await streamPromise; | ||
| await ui.settings.close(); | ||
|
|
||
| expect(timeline.events.some((e) => e.type === "stream-end")).toBe(true); | ||
| await ui.chat.expectTranscriptContains("Python"); | ||
| }); | ||
|
|
||
| test("mode switching doesn't break streaming", async ({ ui }) => { | ||
| await ui.projects.openFirstWorkspace(); | ||
|
|
||
| await ui.chat.setMode("Exec"); | ||
| await ui.chat.setMode("Plan"); | ||
|
|
||
| const timeline = await ui.chat.captureStreamTimeline(async () => { | ||
| await ui.chat.sendMessage(LIST_PROGRAMMING_LANGUAGES); | ||
| }); | ||
|
|
||
| expect(timeline.events.some((e) => e.type === "stream-end")).toBe(true); | ||
| await ui.chat.expectTranscriptContains("Python"); | ||
| }); | ||
|
|
||
| // Consolidate error tests using parameterization | ||
| for (const [errorType, prompt, expectedMessage] of [ | ||
| ["rate limit", ERROR_PROMPTS.TRIGGER_RATE_LIMIT, ERROR_MESSAGES.RATE_LIMIT], | ||
| ["server", ERROR_PROMPTS.TRIGGER_API_ERROR, ERROR_MESSAGES.API_ERROR], | ||
| ["network", ERROR_PROMPTS.TRIGGER_NETWORK_ERROR, ERROR_MESSAGES.NETWORK_ERROR], | ||
| ] as const) { | ||
| test(`${errorType} error displays in transcript`, async ({ ui, page }) => { | ||
| await ui.projects.openFirstWorkspace(); | ||
| await ui.chat.setMode("Exec"); | ||
|
|
||
| const timeline = await ui.chat.captureStreamTimeline(async () => { | ||
| await ui.chat.sendMessage(prompt); | ||
| }); | ||
|
|
||
| expect(timeline.events.some((e) => e.type === "stream-error")).toBe(true); | ||
| const transcript = page.getByRole("log", { name: "Conversation transcript" }); | ||
| await expect(transcript.getByText(expectedMessage)).toBeVisible(); | ||
| }); | ||
| } | ||
|
|
||
| test("app recovers after error", async ({ ui }) => { | ||
| await ui.projects.openFirstWorkspace(); | ||
| await ui.chat.setMode("Exec"); | ||
|
|
||
| await ui.chat.captureStreamTimeline(async () => { | ||
| await ui.chat.sendMessage(ERROR_PROMPTS.TRIGGER_API_ERROR); | ||
| }); | ||
|
|
||
| await ui.chat.setMode("Plan"); | ||
| const timeline = await ui.chat.captureStreamTimeline(async () => { | ||
| await ui.chat.sendMessage(LIST_PROGRAMMING_LANGUAGES); | ||
| }); | ||
|
|
||
| expect(timeline.events.some((e) => e.type === "stream-end")).toBe(true); | ||
| await ui.chat.expectTranscriptContains("Python"); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,61 @@ | ||
| import { electronTest as test, electronExpect as expect } from "../electronTest"; | ||
| import { LIST_PROGRAMMING_LANGUAGES } from "@/node/services/mock/scenarios/basicChat"; | ||
|
|
||
| test.skip( | ||
| ({ browserName }) => browserName !== "chromium", | ||
| "Electron scenario runs on chromium only" | ||
| ); | ||
|
|
||
| test.describe("window lifecycle", () => { | ||
| test("window opens with expected structure", async ({ page }) => { | ||
| await expect(page.getByRole("navigation", { name: "Projects" })).toBeVisible(); | ||
| await expect(page.locator("main, #root, .app-container").first()).toBeVisible(); | ||
| await expect(page.getByRole("dialog", { name: /error/i })).not.toBeVisible(); | ||
| }); | ||
|
|
||
| test("workspace content loads correctly", async ({ ui, page }) => { | ||
| await ui.projects.openFirstWorkspace(); | ||
| await expect(page.getByRole("log", { name: "Conversation transcript" })).toBeVisible(); | ||
| await expect(page.getByRole("textbox", { name: /message/i })).toBeVisible(); | ||
| }); | ||
|
|
||
| test("survives rapid settings navigation", async ({ ui, page }) => { | ||
| await ui.projects.openFirstWorkspace(); | ||
|
|
||
| // Stress test settings modal with rapid open/close/navigate | ||
| for (let i = 0; i < 3; i++) { | ||
| await ui.settings.open(); | ||
| await ui.settings.selectSection("Providers"); | ||
| await ui.settings.selectSection("Models"); | ||
| await ui.settings.close(); | ||
| } | ||
|
|
||
| // Verify app remains functional | ||
| await expect(page.getByRole("navigation", { name: "Projects" })).toBeVisible(); | ||
| const chatInput = page.getByRole("textbox", { name: /message/i }); | ||
| await expect(chatInput).toBeVisible(); | ||
| await chatInput.click(); | ||
| await expect(chatInput).toBeFocused(); | ||
| }); | ||
|
|
||
| // Exercises IPC handler stability under heavy use (regression: #851 duplicate handler registration) | ||
| test("IPC stable after heavy operations", async ({ ui, page }) => { | ||
| await ui.projects.openFirstWorkspace(); | ||
|
|
||
| // Many IPC calls: stream + mode switches + settings navigation | ||
| const timeline = await ui.chat.captureStreamTimeline(async () => { | ||
| await ui.chat.sendMessage(LIST_PROGRAMMING_LANGUAGES); | ||
| }); | ||
| expect(timeline.events.some((e) => e.type === "stream-end")).toBe(true); | ||
|
|
||
| await ui.chat.setMode("Exec"); | ||
| await ui.chat.setMode("Plan"); | ||
| await ui.settings.open(); | ||
| await ui.settings.selectSection("Providers"); | ||
| await ui.settings.close(); | ||
|
|
||
| // Verify app remains functional after all IPC calls | ||
| await expect(page.getByRole("navigation", { name: "Projects" })).toBeVisible(); | ||
| await ui.chat.expectTranscriptContains("Python"); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.