diff --git a/e2e/sdkFieldLayout.spec.ts b/e2e/sdkFieldLayout.spec.ts new file mode 100644 index 000000000..2a56a8551 --- /dev/null +++ b/e2e/sdkFieldLayout.spec.ts @@ -0,0 +1,84 @@ +import { expect, test } from "./fixtures"; + +for (const [id, key, name] of [ + ["languages.node", "languages.node.interpreter", "Node interpreter"], + ["languages.java", "languages.java.jdk", "Project JDK"], + ["languages.php", "languages.php.interpreter", "PHP interpreter"], +]) { + test(`${name} keeps its value and browse controls inside their field`, async ({ workbench: page }) => { + await page.keyboard.press("Meta+,"); + await expect(page.getByRole("dialog", { name: "Settings", exact: true })).toBeVisible(); + await page.evaluate( + async ({ id, key }) => { + const path = "/src/features/settings/model/staging.ts"; + const { selectPage, stage } = await import(/* @vite-ignore */ path); + stage(id, "project", key, "/project/" + "long-path/".repeat(15) + "runtime"); + selectPage(id); + }, + { id, key }, + ); + const combo = page + .getByRole("dialog", { name: "Settings", exact: true }) + .getByRole("button", { name, exact: true }); + await expect(combo).toBeVisible(); + for (const width of [1000, 800, 520]) { + await page.setViewportSize({ width, height: 760 }); + const geometry = await combo.evaluate((el) => { + const field = el.parentElement!; + const bounds = field.getBoundingClientRect(); + return [...field.querySelectorAll("button")].map((button) => { + const r = button.getBoundingClientRect(); + return { + left: r.left, + right: r.right, + width: r.width, + fieldLeft: bounds.left, + fieldRight: bounds.right, + fits: r.width > 0 && r.left >= bounds.left - 1 && r.right <= bounds.right + 1, + }; + }); + }); + expect( + geometry.every((r) => r.fits), + JSON.stringify({ width, geometry }), + ).toBe(true); + } + }); +} + +test("Python interpreter controls stay separate and visible in a narrow settings pane", async ({ workbench: page }) => { + await page.keyboard.press("Meta+,"); + await expect(page.getByRole("dialog", { name: "Settings", exact: true })).toBeVisible(); + await page.evaluate(async () => { + const path = "/src/features/settings/model/staging.ts"; + const { selectPage, stage } = await import(/* @vite-ignore */ path); + stage( + "python.interpreter", + "project", + "languages.python.interpreter", + "/project/" + "long-path/".repeat(15) + "python", + ); + selectPage("python.interpreter"); + }); + const dialog = page.getByRole("dialog", { name: "Settings", exact: true }); + const interpreter = dialog.getByRole("button", { name: "Python interpreter", exact: true }); + const add = dialog.getByRole("button", { name: "Add Interpreter…", exact: true }); + await expect(interpreter).toBeVisible(); + for (const width of [1000, 800, 520]) { + await page.setViewportSize({ width, height: 760 }); + const combo = await interpreter.boundingBox(); + const button = await add.boundingBox(); + const row = await add.evaluate((el) => { + const r = el.parentElement!.getBoundingClientRect(); + return { left: r.left, right: r.right }; + }); + expect(combo!.width).toBeGreaterThan(20); + expect(combo!.x).toBeGreaterThanOrEqual(row.left - 1); + expect(combo!.x + combo!.width).toBeLessThanOrEqual(row.right + 1); + expect(button!.x + button!.width).toBeLessThanOrEqual(row.right + 1); + const overlap = + Math.min(combo!.x + combo!.width, button!.x + button!.width) > Math.max(combo!.x, button!.x) && + Math.min(combo!.y + combo!.height, button!.y + button!.height) > Math.max(combo!.y, button!.y); + expect(overlap).toBe(false); + } +}); diff --git a/src/features/settings/languages/Sdk.module.css b/src/features/settings/languages/Sdk.module.css index 625d512c1..63087b9e5 100644 --- a/src/features/settings/languages/Sdk.module.css +++ b/src/features/settings/languages/Sdk.module.css @@ -9,21 +9,25 @@ display: inline-flex; align-items: center; gap: 4px; - min-width: 0; + min-width: 180px; flex: 1; max-width: 560px; } .sdkSelect { flex: 1; - min-width: 260px; + min-width: 0; } .interpreterRow { display: flex; + flex-wrap: wrap; align-items: center; gap: 8px; min-height: var(--h-row); } +.interpreterRow > .sdkField { + flex-basis: 260px; +} .interpreterLabel { white-space: nowrap; color: var(--fg);