Skip to content

Commit b536a37

Browse files
committed
🤖 ci: port Storybook settings/errors interactions to jsdom harness
1 parent 95cf86d commit b536a37

File tree

2 files changed

+188
-0
lines changed

2 files changed

+188
-0
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/**
2+
* Ports of Storybook interaction tests from `src/browser/stories/App.errors.stories.tsx`.
3+
*/
4+
5+
import "@testing-library/jest-dom";
6+
import { waitFor } from "@testing-library/react";
7+
import userEvent from "@testing-library/user-event";
8+
import { shouldRunIntegrationTests } from "../../testUtils";
9+
import {
10+
renderWithBackend,
11+
waitForAppLoad,
12+
addProjectViaUI,
13+
createTempGitRepo,
14+
cleanupTempGitRepo,
15+
getProjectName,
16+
} from "../harness";
17+
18+
const describeIntegration = shouldRunIntegrationTests() ? describe : describe.skip;
19+
20+
describeIntegration("Storybook interactions: Errors", () => {
21+
test("ProjectRemovalError: removing a project with active workspaces shows an error alert", async () => {
22+
const user = userEvent.setup();
23+
const gitRepo = await createTempGitRepo();
24+
25+
const { cleanup, env, ...queries } = await renderWithBackend();
26+
try {
27+
await waitForAppLoad(queries);
28+
29+
await addProjectViaUI(user, queries, gitRepo);
30+
31+
// Create 2 workspaces, so `projects.remove()` returns the same error shown in Storybook.
32+
const ws1 = await env.orpc.workspace.create({
33+
projectPath: gitRepo,
34+
branchName: "feature-auth",
35+
trunkBranch: "main",
36+
});
37+
const ws2 = await env.orpc.workspace.create({
38+
projectPath: gitRepo,
39+
branchName: "feature-ui",
40+
trunkBranch: "main",
41+
});
42+
expect(ws1.success && ws2.success).toBe(true);
43+
if (!ws1.success || !ws2.success) throw new Error("Workspace creation failed");
44+
45+
const projectName = getProjectName(gitRepo);
46+
47+
// The remove button is typically shown on hover; select via aria-label (same as Storybook).
48+
await waitFor(() => {
49+
const btn = document.querySelector(`button[aria-label="Remove project ${projectName}"]`);
50+
expect(btn).toBeTruthy();
51+
});
52+
53+
const removeButton = document.querySelector(
54+
`button[aria-label="Remove project ${projectName}"]`
55+
) as HTMLElement;
56+
const projectRow = removeButton.closest("[data-project-path]") as HTMLElement | null;
57+
expect(projectRow).toBeTruthy();
58+
59+
await user.hover(projectRow!);
60+
61+
await waitFor(() => {
62+
expect(removeButton).toBeVisible();
63+
});
64+
65+
// This flow intentionally triggers an error path. Silence the expected
66+
// console.error line while preserving unexpected errors.
67+
const consoleErrorMock = console.error as unknown as jest.Mock;
68+
const previousImpl = consoleErrorMock.getMockImplementation();
69+
consoleErrorMock.mockImplementation((...args: unknown[]) => {
70+
const text = args.map(String).join(" ");
71+
if (
72+
text.includes(
73+
"Failed to remove project: Cannot remove project with active workspaces. Please remove all 2 workspace(s) first."
74+
)
75+
) {
76+
return;
77+
}
78+
return previousImpl?.(...args);
79+
});
80+
try {
81+
await user.click(removeButton);
82+
83+
const alert = await queries.findByRole("alert");
84+
expect(alert).toHaveTextContent(
85+
/cannot remove project with active workspaces\. please remove all 2 workspace\(s\) first\./i
86+
);
87+
} finally {
88+
consoleErrorMock.mockImplementation(previousImpl ?? (() => {}));
89+
}
90+
} finally {
91+
await cleanupTempGitRepo(gitRepo);
92+
await cleanup();
93+
}
94+
});
95+
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Ports of Storybook interaction tests from `src/browser/stories/App.settings.stories.tsx`.
3+
*
4+
* These tests run in Jest + jsdom using the browser UI harness (real ServiceContainer + oRPC).
5+
*
6+
* Goal: keep the same user-facing flows as Storybook play functions, but with a real backend.
7+
*/
8+
9+
import "@testing-library/jest-dom";
10+
import { within, waitFor } from "@testing-library/react";
11+
import userEvent from "@testing-library/user-event";
12+
import { shouldRunIntegrationTests } from "../../testUtils";
13+
import {
14+
createBrowserTestEnv,
15+
renderWithBackend,
16+
waitForAppLoad,
17+
openSettingsToSection,
18+
} from "../harness";
19+
20+
const describeIntegration = shouldRunIntegrationTests() ? describe : describe.skip;
21+
22+
describeIntegration("Storybook interactions: Settings", () => {
23+
test("Providers section reflects configured provider state", async () => {
24+
const user = userEvent.setup();
25+
const env = await createBrowserTestEnv();
26+
27+
// Seed provider config before rendering.
28+
const setKey = await env.orpc.providers.setProviderConfig({
29+
provider: "openai",
30+
keyPath: ["apiKey"],
31+
value: "test-api-key",
32+
});
33+
expect(setKey.success).toBe(true);
34+
35+
const setBaseUrl = await env.orpc.providers.setProviderConfig({
36+
provider: "openai",
37+
keyPath: ["baseUrl"],
38+
value: "https://custom.openai.com/v1",
39+
});
40+
expect(setBaseUrl.success).toBe(true);
41+
42+
const { cleanup, ...queries } = await renderWithBackend({ env });
43+
try {
44+
await waitForAppLoad(queries);
45+
46+
const modal = await openSettingsToSection(user, queries, "Providers");
47+
48+
// Expand OpenAI provider row
49+
const openaiButton = within(modal).getByRole("button", { name: /openai/i });
50+
await user.click(openaiButton);
51+
52+
// Config dot is title-tagged.
53+
expect(openaiButton.querySelector('[title="Configured"]')).toBeInTheDocument();
54+
55+
// Base URL should show the seeded value.
56+
await waitFor(() => {
57+
expect(within(modal).getByText("https://custom.openai.com/v1")).toBeInTheDocument();
58+
});
59+
60+
// API key should show as masked.
61+
expect(within(modal).getByText("••••••••")).toBeInTheDocument();
62+
} finally {
63+
await cleanup();
64+
}
65+
});
66+
67+
test("Models section shows custom models from provider config", async () => {
68+
const user = userEvent.setup();
69+
const env = await createBrowserTestEnv();
70+
71+
const setModels = await env.orpc.providers.setModels({
72+
provider: "openai",
73+
models: ["my-custom-model-123"],
74+
});
75+
expect(setModels.success).toBe(true);
76+
77+
const { cleanup, ...queries } = await renderWithBackend({ env });
78+
try {
79+
await waitForAppLoad(queries);
80+
81+
const modal = await openSettingsToSection(user, queries, "Models");
82+
83+
// Wait for the models UI to load and display custom models.
84+
await waitFor(() => {
85+
expect(within(modal).getByText(/custom models/i)).toBeInTheDocument();
86+
});
87+
88+
expect(within(modal).getByText("my-custom-model-123")).toBeInTheDocument();
89+
} finally {
90+
await cleanup();
91+
}
92+
});
93+
});

0 commit comments

Comments
 (0)