Files
kandev/apps/web/e2e/tests/integrations/linear-settings.spec.ts
T
João Salavisa 5936bb6808 test: add e2e coverage and mock backends for jira and linear integrations (#771)
* test: add e2e coverage and mock backends for jira and linear integrations

Adds in-memory MockClient + MockController for both integrations gated by
KANDEV_MOCK_{JIRA,LINEAR}=true, plus apiClient mock helpers and Playwright
specs covering settings and import flows.

Also fixes ValidatedPopover's TriggerButton silently dropping radix Slot's
onClick handler — the popover wouldn't open under any synthetic-click path
(including Playwright). Spreads `...rest` onto the inner Button.

Reworks the integration cleanup hook in test-base as an auto fixture so it
runs for every test using the imported `test` object, not just tests in the
fixture file itself.

* fix: address PR review feedback on jira/linear mocks and e2e specs

- Fix SetAuthResult docstrings: nil simulates an unconfigured state, not the default success.
- Drop unused JSON tags from doneTransitionCall and setStateCall (Go-only test fixtures).
- Track Linear issue insertion order so SearchIssues returns deterministic results instead of map-iteration shuffle.
- Document filterByJQL precedence at the SearchTickets call site so the all-hits-on-no-match fallback is visible.
- Wait for the post-save auth-health probe before forcing a failure in the seeded-failure tests, eliminating a goroutine race.
- Reorder authMethod precedence in setJiraConfig/setLinearConfig to put the spread first so 'caller wins' reads naturally.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
2026-05-02 14:41:36 +01:00

110 lines
4.3 KiB
TypeScript

import { test, expect } from "../../fixtures/test-base";
import { LinearSettingsPage } from "../../pages/linear-settings-page";
test.describe("Linear settings", () => {
test("empty workspace shows form with disabled save/test until secret is filled", async ({
testPage,
seedData,
}) => {
const settings = new LinearSettingsPage(testPage);
await settings.goto(seedData.workspaceId);
await expect(settings.secretInput).toHaveValue("");
await expect(settings.statusBanner).toHaveCount(0);
await expect(settings.saveButton).toBeDisabled();
await expect(settings.testButton).toBeDisabled();
await settings.secretInput.fill("lin_api_xxx");
await expect(settings.saveButton).toBeEnabled();
await expect(settings.testButton).toBeEnabled();
});
test("saving the config persists across reload and shows the auth banner", async ({
testPage,
seedData,
apiClient,
}) => {
// Seed a single team so the dropdown post-save can populate without an
// empty-list flash. The default-team field is optional, but the team
// fetch fires when hasSecret flips true and we want it to succeed.
await apiClient.mockLinearSetTeams([{ id: "team-1", key: "ENG", name: "Engineering" }]);
const settings = new LinearSettingsPage(testPage);
await settings.goto(seedData.workspaceId);
await settings.secretInput.fill("lin_api_xxx");
await settings.saveButton.click();
await expect(settings.saveButton).toHaveText(/Update/i);
// Wait for the async post-save probe to write lastOk=true before reloading.
await apiClient.waitForIntegrationAuthHealthy("linear", seedData.workspaceId);
await testPage.reload();
await settings.secretInput.waitFor();
await expect(settings.statusBanner).toHaveAttribute("data-state", "ok");
// Saved-secret hint indicates the row was loaded, not started fresh.
await expect(testPage.getByText(/leave blank to keep the current value/i)).toBeVisible();
});
test("test connection surfaces inline success and failure", async ({
testPage,
seedData,
apiClient,
}) => {
const settings = new LinearSettingsPage(testPage);
await settings.goto(seedData.workspaceId);
await apiClient.mockLinearSetAuthResult({
ok: true,
displayName: "Alice from Linear",
email: "alice@example.com",
orgName: "Acme",
});
await settings.secretInput.fill("lin_api_xxx");
await settings.testButton.click();
await expect(testPage.getByText(/Connected as Alice from Linear/i)).toBeVisible();
await apiClient.mockLinearSetAuthResult({ ok: false, error: "Bad token" });
await settings.testButton.click();
await expect(testPage.getByText(/Failed: Bad token/)).toBeVisible();
});
test("seeded auth-health failure renders the failed banner on load", async ({
testPage,
seedData,
apiClient,
}) => {
const settings = new LinearSettingsPage(testPage);
await settings.goto(seedData.workspaceId);
await settings.secretInput.fill("lin_api_xxx");
await settings.saveButton.click();
// Wait for the post-save probe to land BEFORE forcing the failure: the
// probe goroutine could otherwise overwrite our forced lastOk=false back
// to true a few ms after the mockLinearSetAuthHealth call.
await apiClient.waitForIntegrationAuthHealthy("linear", seedData.workspaceId);
await apiClient.mockLinearSetAuthHealth({
workspaceId: seedData.workspaceId,
ok: false,
error: "rate limited",
});
await testPage.reload();
await settings.statusBanner.waitFor();
await expect(settings.statusBanner).toHaveAttribute("data-state", "failed");
await expect(settings.statusBanner).toContainText(/rate limited/i);
});
test("delete clears the saved configuration", async ({ testPage, seedData }) => {
const settings = new LinearSettingsPage(testPage);
await settings.goto(seedData.workspaceId);
await settings.secretInput.fill("lin_api_xxx");
await settings.saveButton.click();
await expect(settings.deleteButton).toBeVisible();
testPage.once("dialog", (d) => void d.accept());
await settings.deleteButton.click();
await expect(settings.deleteButton).toHaveCount(0);
await expect(settings.secretInput).toHaveValue("");
await expect(settings.statusBanner).toHaveCount(0);
});
});