diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index bdfc8b0153..6525fa3b01 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -13,29 +13,135 @@ import { Provision_Complete, Provision_Response, Resource, + RichParameter, } from "./provisionerGenerated" import { port } from "./playwright.config" import * as ssh from "ssh2" import { Duplex } from "stream" +import { WorkspaceBuildParameter } from "api/typesGenerated" // createWorkspace creates a workspace for a template. // It does not wait for it to be running, but it does navigate to the page. export const createWorkspace = async ( page: Page, templateName: string, + richParameters: RichParameter[] = [], + buildParameters: WorkspaceBuildParameter[] = [], ): Promise => { await page.goto("/templates/" + templateName + "/workspace", { waitUntil: "networkidle", }) const name = randomName() await page.getByLabel("name").fill(name) + + for (const buildParameter of buildParameters) { + const richParameter = richParameters.find( + (richParam) => richParam.name === buildParameter.name, + ) + if (!richParameter) { + throw new Error( + "build parameter is expected to be present in rich parameter schema", + ) + } + + const parameterLabel = await page.waitForSelector( + "[data-testid='parameter-field-" + richParameter.name + "']", + { state: "visible" }, + ) + + if (richParameter.type === "bool") { + const parameterField = await parameterLabel.waitForSelector( + "[data-testid='parameter-field-bool'] .MuiRadio-root input[value='" + + buildParameter.value + + "']", + ) + await parameterField.check() + } else if (richParameter.options.length > 0) { + const parameterField = await parameterLabel.waitForSelector( + "[data-testid='parameter-field-options'] .MuiRadio-root input[value='" + + buildParameter.value + + "']", + ) + await parameterField.check() + } else if (richParameter.type === "list(string)") { + throw new Error("not implemented yet") // FIXME + } else { + // text or number + const parameterField = await parameterLabel.waitForSelector( + "[data-testid='parameter-field-text'] input", + ) + await parameterField.fill(buildParameter.value) + } + } + await page.getByTestId("form-submit").click() await expect(page).toHaveURL("/@admin/" + name) - await page.getByTestId("build-status").isVisible() + await page.waitForSelector("[data-testid='build-status']", { + state: "visible", + }) return name } +export const verifyParameters = async ( + page: Page, + workspaceName: string, + richParameters: RichParameter[], + expectedBuildParameters: WorkspaceBuildParameter[], +) => { + await page.goto("/@admin/" + workspaceName + "/settings/parameters", { + waitUntil: "networkidle", + }) + await expect(page).toHaveURL( + "/@admin/" + workspaceName + "/settings/parameters", + ) + + for (const buildParameter of expectedBuildParameters) { + const richParameter = richParameters.find( + (richParam) => richParam.name === buildParameter.name, + ) + if (!richParameter) { + throw new Error( + "build parameter is expected to be present in rich parameter schema", + ) + } + + const parameterLabel = await page.waitForSelector( + "[data-testid='parameter-field-" + richParameter.name + "']", + { state: "visible" }, + ) + + const muiDisabled = richParameter.mutable ? "" : ".Mui-disabled" + + if (richParameter.type === "bool") { + const parameterField = await parameterLabel.waitForSelector( + "[data-testid='parameter-field-bool'] .MuiRadio-root.Mui-checked" + + muiDisabled + + " input", + ) + const value = await parameterField.inputValue() + expect(value).toEqual(buildParameter.value) + } else if (richParameter.options.length > 0) { + const parameterField = await parameterLabel.waitForSelector( + "[data-testid='parameter-field-options'] .MuiRadio-root.Mui-checked" + + muiDisabled + + " input", + ) + const value = await parameterField.inputValue() + expect(value).toEqual(buildParameter.value) + } else if (richParameter.type === "list(string)") { + throw new Error("not implemented yet") // FIXME + } else { + // text or number + const parameterField = await parameterLabel.waitForSelector( + "[data-testid='parameter-field-text'] input" + muiDisabled, + ) + const value = await parameterField.inputValue() + expect(value).toEqual(buildParameter.value) + } + } +} + // createTemplate navigates to the /templates/new page and uploads a template // with the resources provided in the responses argument. export const createTemplate = async ( @@ -401,3 +507,28 @@ const findSessionToken = async (page: Page): Promise => { } return sessionCookie.value } + +export const echoResponsesWithParameters = ( + richParameters: RichParameter[], +): EchoProvisionerResponses => { + return { + plan: [ + { + complete: { + parameters: richParameters, + }, + }, + ], + apply: [ + { + complete: { + resources: [ + { + name: "example", + }, + ], + }, + }, + ], + } +} diff --git a/site/e2e/parameters.ts b/site/e2e/parameters.ts new file mode 100644 index 0000000000..c575fdcf83 --- /dev/null +++ b/site/e2e/parameters.ts @@ -0,0 +1,138 @@ +import { RichParameter } from "./provisionerGenerated" + +// Rich parameters + +const emptyParameter: RichParameter = { + name: "", + description: "", + type: "", + mutable: false, + defaultValue: "", + icon: "", + options: [], + validationRegex: "", + validationError: "", + validationMin: undefined, + validationMax: undefined, + validationMonotonic: "", + required: false, + displayName: "", + order: 0, + ephemeral: false, +} + +// firstParameter is mutable string with a default value (parameter value not required). +export const firstParameter: RichParameter = { + ...emptyParameter, + + name: "first_parameter", + displayName: "First parameter", + type: "number", + options: [], + description: "This is first parameter.", + icon: "/emojis/1f310.png", + defaultValue: "123", + mutable: true, + order: 1, +} + +// secondParameter is immutable string with a default value (parameter value not required). +export const secondParameter: RichParameter = { + ...emptyParameter, + + name: "second_parameter", + displayName: "Second parameter", + type: "string", + options: [], + description: "This is second parameter.", + defaultValue: "abc", + icon: "", + order: 2, +} + +// thirdParameter is mutable string with an empty default value (parameter value not required). +export const thirdParameter: RichParameter = { + ...emptyParameter, + + name: "third_parameter", + type: "string", + options: [], + description: "This is third parameter.", + defaultValue: "", + mutable: true, + order: 3, +} + +// fourthParameter is immutable boolean with a default "true" value (parameter value not required). +export const fourthParameter: RichParameter = { + ...emptyParameter, + + name: "fourth_parameter", + type: "bool", + options: [], + description: "This is fourth parameter.", + defaultValue: "true", + icon: "", + order: 3, +} + +// fifthParameter is immutable "string with options", with a default option selected (parameter value not required). +export const fifthParameter: RichParameter = { + ...emptyParameter, + + name: "fifth_parameter", + displayName: "Fifth parameter", + type: "string", + options: [ + { + name: "ABC", + description: "This is ABC", + value: "abc", + icon: "", + }, + { + name: "DEF", + description: "This is DEF", + value: "def", + icon: "", + }, + { + name: "GHI", + description: "This is GHI", + value: "ghi", + icon: "", + }, + ], + description: "This is fifth parameter.", + defaultValue: "def", + icon: "", + order: 3, +} + +// sixthParameter is mutable string without a default value (parameter value is required). +export const sixthParameter: RichParameter = { + ...emptyParameter, + + name: "sixth_parameter", + displayName: "Sixth parameter", + type: "number", + options: [], + description: "This is sixth parameter.", + icon: "/emojis/1f310.png", + required: true, + mutable: true, + order: 1, +} + +// seventhParameter is immutable string without a default value (parameter value is required). +export const seventhParameter: RichParameter = { + ...emptyParameter, + + name: "seventh_parameter", + displayName: "Seventh parameter", + type: "string", + options: [], + description: "This is seventh parameter.", + required: true, + order: 1, +} diff --git a/site/e2e/tests/createWorkspace.spec.ts b/site/e2e/tests/createWorkspace.spec.ts index 3317691300..10630e2f46 100644 --- a/site/e2e/tests/createWorkspace.spec.ts +++ b/site/e2e/tests/createWorkspace.spec.ts @@ -1,5 +1,21 @@ import { test } from "@playwright/test" -import { createTemplate, createWorkspace } from "../helpers" +import { + createTemplate, + createWorkspace, + echoResponsesWithParameters, + verifyParameters, +} from "../helpers" + +import { + secondParameter, + fourthParameter, + fifthParameter, + firstParameter, + thirdParameter, + seventhParameter, + sixthParameter, +} from "../parameters" +import { RichParameter } from "../provisionerGenerated" test("create workspace", async ({ page }) => { const template = await createTemplate(page, { @@ -17,3 +33,85 @@ test("create workspace", async ({ page }) => { }) await createWorkspace(page, template) }) + +test("create workspace with default immutable parameters", async ({ page }) => { + const richParameters: RichParameter[] = [ + secondParameter, + fourthParameter, + fifthParameter, + ] + const template = await createTemplate( + page, + echoResponsesWithParameters(richParameters), + ) + const workspaceName = await createWorkspace(page, template) + await verifyParameters(page, workspaceName, richParameters, [ + { name: secondParameter.name, value: secondParameter.defaultValue }, + { name: fourthParameter.name, value: fourthParameter.defaultValue }, + { name: fifthParameter.name, value: fifthParameter.defaultValue }, + ]) +}) + +test("create workspace with default mutable parameters", async ({ page }) => { + const richParameters: RichParameter[] = [firstParameter, thirdParameter] + const template = await createTemplate( + page, + echoResponsesWithParameters(richParameters), + ) + const workspaceName = await createWorkspace(page, template) + await verifyParameters(page, workspaceName, richParameters, [ + { name: firstParameter.name, value: firstParameter.defaultValue }, + { name: thirdParameter.name, value: thirdParameter.defaultValue }, + ]) +}) + +test("create workspace with default and required parameters", async ({ + page, +}) => { + const richParameters: RichParameter[] = [ + secondParameter, + fourthParameter, + sixthParameter, + seventhParameter, + ] + const buildParameters = [ + { name: sixthParameter.name, value: "12345" }, + { name: seventhParameter.name, value: "abcdef" }, + ] + const template = await createTemplate( + page, + echoResponsesWithParameters(richParameters), + ) + const workspaceName = await createWorkspace( + page, + template, + richParameters, + buildParameters, + ) + await verifyParameters(page, workspaceName, richParameters, [ + // user values: + ...buildParameters, + // default values: + { name: secondParameter.name, value: secondParameter.defaultValue }, + { name: fourthParameter.name, value: fourthParameter.defaultValue }, + ]) +}) + +test("create workspace and overwrite default parameters", async ({ page }) => { + const richParameters: RichParameter[] = [secondParameter, fourthParameter] + const buildParameters = [ + { name: secondParameter.name, value: "AAAAA" }, + { name: fourthParameter.name, value: "false" }, + ] + const template = await createTemplate( + page, + echoResponsesWithParameters(richParameters), + ) + const workspaceName = await createWorkspace( + page, + template, + richParameters, + buildParameters, + ) + await verifyParameters(page, workspaceName, richParameters, buildParameters) +}) diff --git a/site/src/components/RichParameterInput/RichParameterInput.tsx b/site/src/components/RichParameterInput/RichParameterInput.tsx index 7d5b83894f..4e3a671706 100644 --- a/site/src/components/RichParameterInput/RichParameterInput.tsx +++ b/site/src/components/RichParameterInput/RichParameterInput.tsx @@ -84,6 +84,7 @@ export const RichParameterInput: FC = ({ direction="column" spacing={size === "small" ? 1.25 : 2} className={size} + data-testid={`parameter-field-${parameter.name}`} > @@ -114,6 +115,7 @@ const RichParameterField: React.FC = ({ if (isBoolean(parameter)) { return ( { @@ -139,6 +141,7 @@ const RichParameterField: React.FC = ({ if (parameter.options.length > 0) { return ( { @@ -185,6 +188,7 @@ const RichParameterField: React.FC = ({ return ( { @@ -206,6 +210,7 @@ const RichParameterField: React.FC = ({ return (