@@ -183,34 +183,37 @@ export const verifyParameters = async (
183183) ;
184184}
185185
186- const parameterLabel = await page . waitForSelector (
187- `[data-testid='parameter-field-${ richParameter . name } ']` ,
188- { state :"visible" } ,
186+ const parameterLabel = page . getByTestId (
187+ `parameter-field-${ richParameter . displayName } ` ,
189188) ;
189+ await expect ( parameterLabel ) . toBeVisible ( ) ;
190190
191- const muiDisabled = richParameter . mutable ?"" :".Mui-disabled" ;
191+ if ( richParameter . options . length > 0 ) {
192+ const parameterValue = parameterLabel . getByLabel ( buildParameter . value ) ;
193+ const value = await parameterValue . isChecked ( ) ;
194+ expect ( value ) . toBe ( true ) ;
195+ continue ;
196+ }
192197
193- if ( richParameter . type === "bool" ) {
194- const parameterField = await parameterLabel . waitForSelector (
195- `[data-testid='parameter-field-bool'] .MuiRadio-root.Mui-checked${ muiDisabled } input` ,
196- ) ;
197- const value = await parameterField . inputValue ( ) ;
198- expect ( value ) . toEqual ( buildParameter . value ) ;
199- } else if ( richParameter . options . length > 0 ) {
200- const parameterField = await parameterLabel . waitForSelector (
201- `[data-testid='parameter-field-options'] .MuiRadio-root.Mui-checked${ muiDisabled } input` ,
202- ) ;
203- const value = await parameterField . inputValue ( ) ;
204- expect ( value ) . toEqual ( buildParameter . value ) ;
205- } else if ( richParameter . type === "list(string)" ) {
206- throw new Error ( "not implemented yet" ) ; // FIXME
207- } else {
208- // text or number
209- const parameterField = await parameterLabel . waitForSelector (
210- `[data-testid='parameter-field-text'] input${ muiDisabled } ` ,
211- ) ;
212- const value = await parameterField . inputValue ( ) ;
213- expect ( value ) . toEqual ( buildParameter . value ) ;
198+ switch ( richParameter . type ) {
199+ case "bool" :
200+ {
201+ const parameterField = parameterLabel . locator ( "input" ) ;
202+ const value = await parameterField . isChecked ( ) ;
203+ expect ( value . toString ( ) ) . toEqual ( buildParameter . value ) ;
204+ }
205+ break ;
206+ case "string" :
207+ case "number" :
208+ {
209+ const parameterField = parameterLabel . locator ( "input" ) ;
210+ const value = await parameterField . inputValue ( ) ;
211+ expect ( value ) . toEqual ( buildParameter . value ) ;
212+ }
213+ break ;
214+ default :
215+ // Some types like `list(string)` are not tested
216+ throw new Error ( "not implemented yet" ) ;
214217}
215218}
216219} ;
@@ -373,25 +376,22 @@ export const stopWorkspace = async (page: Page, workspaceName: string) => {
373376} ) ;
374377} ;
375378
376- export const buildWorkspaceWithParameters = async (
379+ export const startWorkspaceWithEphemeralParameters = async (
377380page :Page ,
378381workspaceName :string ,
379382richParameters :RichParameter [ ] = [ ] ,
380383buildParameters :WorkspaceBuildParameter [ ] = [ ] ,
381- confirm = false ,
382384) => {
383385const user = currentUser ( page ) ;
384386await page . goto ( `/@${ user . username } /${ workspaceName } ` , {
385387waitUntil :"domcontentloaded" ,
386388} ) ;
387389
388- await page . getByTestId ( "build-parameters-button" ) . click ( ) ;
390+ await page . getByTestId ( "workspace-start" ) . click ( ) ;
391+ await page . getByTestId ( "workspace-parameters" ) . click ( ) ;
389392
390393await fillParameters ( page , richParameters , buildParameters ) ;
391- await page . getByTestId ( "build-parameters-submit" ) . click ( ) ;
392- if ( confirm ) {
393- await page . getByTestId ( "confirm-button" ) . click ( ) ;
394- }
394+ await page . getByRole ( "button" , { name :"Update and restart" } ) . click ( ) ;
395395
396396await page . waitForSelector ( "text=Workspace status: Running" , {
397397state :"visible" ,
@@ -547,6 +547,9 @@ interface EchoProvisionerResponses {
547547plan ?:RecursivePartial < Response > [ ] ;
548548// apply occurs when the workspace is built
549549apply ?:RecursivePartial < Response > [ ] ;
550+ // extraFiles allows the bundling of terraform files in echo provisioner tars
551+ // in order to support dynamic parameters
552+ extraFiles ?:Map < string , string > ;
550553}
551554
552555const emptyPlan = new TextEncoder ( ) . encode ( "{}" ) ;
@@ -595,6 +598,13 @@ const createTemplateVersionTar = async (
595598}
596599
597600const tar = new TarWriter ( ) ;
601+
602+ if ( responses . extraFiles ) {
603+ for ( const [ fileName , fileContents ] of responses . extraFiles ) {
604+ tar . addFile ( fileName , fileContents ) ;
605+ }
606+ }
607+
598608responses . parse . forEach ( ( response , index ) => {
599609response . parse = {
600610templateVariables :[ ] ,
@@ -830,6 +840,50 @@ export const findSessionToken = async (page: Page): Promise<string> => {
830840export const echoResponsesWithParameters = (
831841richParameters :RichParameter [ ] ,
832842) :EchoProvisionerResponses => {
843+ let tf = `terraform {
844+ required_providers {
845+ coder = {
846+ source = "coder/coder"
847+ }
848+ }
849+ }
850+ ` ;
851+
852+ for ( const parameter of richParameters ) {
853+ let options = "" ;
854+ if ( parameter . options ) {
855+ for ( const option of parameter . options ) {
856+ options += `
857+ option {
858+ name =${ JSON . stringify ( option . name ) }
859+ description =${ JSON . stringify ( option . description ) }
860+ value =${ JSON . stringify ( option . value ) }
861+ icon =${ JSON . stringify ( option . icon ) }
862+ }
863+ ` ;
864+ }
865+ }
866+
867+ tf += `
868+ data "coder_parameter" "${ parameter . name } " {
869+ type =${ JSON . stringify ( parameter . type ) }
870+ name =${ JSON . stringify ( parameter . displayName ) }
871+ icon =${ JSON . stringify ( parameter . icon ) }
872+ description =${ JSON . stringify ( parameter . description ) }
873+ mutable =${ JSON . stringify ( parameter . mutable ) } ` ;
874+
875+ if ( ! parameter . required ) {
876+ tf += `
877+ default =${ JSON . stringify ( parameter . defaultValue ) } ` ;
878+ }
879+
880+ tf += `
881+ order =${ JSON . stringify ( parameter . order ) }
882+ ephemeral =${ JSON . stringify ( parameter . ephemeral ) }
883+ ${ options } }
884+ ` ;
885+ }
886+
833887return {
834888parse :[
835889{
@@ -854,6 +908,7 @@ export const echoResponsesWithParameters = (
854908} ,
855909} ,
856910] ,
911+ extraFiles :new Map ( [ [ "main.tf" , tf ] ] ) ,
857912} ;
858913} ;
859914
@@ -903,30 +958,36 @@ const fillParameters = async (
903958) ;
904959}
905960
906- // Use modern locator approach instead of waitForSelector
907961const parameterLabel = page . getByTestId (
908- `parameter-field-${ richParameter . name } ` ,
962+ `parameter-field-${ richParameter . displayName } ` ,
909963) ;
910964await expect ( parameterLabel ) . toBeVisible ( ) ;
911965
912- if ( richParameter . type === "bool" ) {
913- const parameterField = parameterLabel
914- . getByTestId ( "parameter-field-bool" )
915- . locator ( `.MuiRadio-root input[value='${ buildParameter . value } ']` ) ;
916- await parameterField . click ( ) ;
917- } else if ( richParameter . options . length > 0 ) {
918- const parameterField = parameterLabel
919- . getByTestId ( "parameter-field-options" )
920- . locator ( `.MuiRadio-root input[value='${ buildParameter . value } ']` ) ;
921- await parameterField . click ( ) ;
922- } else if ( richParameter . type === "list(string)" ) {
923- throw new Error ( "not implemented yet" ) ; // FIXME
924- } else {
925- // text or number
926- const parameterField = parameterLabel
927- . getByTestId ( "parameter-field-text" )
928- . locator ( "input" ) ;
929- await parameterField . fill ( buildParameter . value ) ;
966+ if ( richParameter . options . length > 0 ) {
967+ const parameterValue = parameterLabel . getByRole ( "button" , {
968+ name :buildParameter . value ,
969+ } ) ;
970+ await parameterValue . click ( ) ;
971+ continue ;
972+ }
973+
974+ switch ( richParameter . type ) {
975+ case "bool" :
976+ {
977+ const parameterField = parameterLabel . locator ( "button" ) ;
978+ await parameterField . click ( ) ;
979+ }
980+ break ;
981+ case "string" :
982+ case "number" :
983+ {
984+ const parameterField = parameterLabel . locator ( "input" ) ;
985+ await parameterField . fill ( buildParameter . value ) ;
986+ }
987+ break ;
988+ default :
989+ // Some types like `list(string)` are not tested
990+ throw new Error ( "not implemented yet" ) ;
930991}
931992}
932993} ;
@@ -1021,27 +1082,13 @@ export const updateWorkspace = async (
10211082await page . getByTestId ( "workspace-update-button" ) . click ( ) ;
10221083await page . getByTestId ( "confirm-button" ) . click ( ) ;
10231084
1024- await page . waitForSelector ( '[data-testid="dialog"]' , { state :"visible" } ) ;
1085+ await page
1086+ . getByRole ( "button" , { name :/ g o t o w o r k s p a c e p a r a m e t e r s / i} )
1087+ . click ( ) ;
10251088
10261089await fillParameters ( page , richParameters , buildParameters ) ;
1027- await page . getByRole ( "button" , { name :/ u p d a t e p a r a m e t e r s / i} ) . click ( ) ;
10281090
1029- // Wait for the update button to detach.
1030- await page . waitForSelector (
1031- "button[data-testid='workspace-update-button']:enabled" ,
1032- { state :"detached" } ,
1033- ) ;
1034- // Wait for the workspace to be running again.
1035- await page . waitForSelector ( "text=Workspace status: Running" , {
1036- state :"visible" ,
1037- } ) ;
1038- // Wait for the stop button to be enabled again
1039- await page . waitForSelector (
1040- "button[data-testid='workspace-stop-button']:enabled" ,
1041- {
1042- state :"visible" ,
1043- } ,
1044- ) ;
1091+ await page . getByRole ( "button" , { name :/ u p d a t e a n d r e s t a r t / i} ) . click ( ) ;
10451092} ;
10461093
10471094export const updateWorkspaceParameters = async (
@@ -1056,7 +1103,7 @@ export const updateWorkspaceParameters = async (
10561103} ) ;
10571104
10581105await fillParameters ( page , richParameters , buildParameters ) ;
1059- await page . getByRole ( "button" , { name :/ s u b m i t a n d r e s t a r t / i} ) . click ( ) ;
1106+ await page . getByRole ( "button" , { name :/ u p d a t e a n d r e s t a r t / i} ) . click ( ) ;
10601107
10611108await page . waitForSelector ( "text=Workspace status: Running" , {
10621109state :"visible" ,
@@ -1209,48 +1256,3 @@ export async function addUserToOrganization(
12091256}
12101257await page . mouse . click ( 10 , 10 ) ; // close the popover by clicking outside of it
12111258}
1212-
1213- /**
1214- * disableDynamicParameters navigates to the template settings page and disables
1215- * dynamic parameters by unchecking the "Enable dynamic parameters" checkbox.
1216- */
1217- export const disableDynamicParameters = async (
1218- page :Page ,
1219- templateName :string ,
1220- orgName = defaultOrganizationName ,
1221- ) => {
1222- await page . goto ( `/templates/${ orgName } /${ templateName } /settings` , {
1223- waitUntil :"domcontentloaded" ,
1224- } ) ;
1225-
1226- await page . waitForSelector ( "form" , { state :"visible" } ) ;
1227-
1228- // Find and uncheck the "Enable dynamic parameters" checkbox
1229- const dynamicParamsCheckbox = page . getByRole ( "checkbox" , {
1230- name :/ E n a b l e d y n a m i c p a r a m e t e r s f o r w o r k s p a c e c r e a t i o n / ,
1231- } ) ;
1232-
1233- await dynamicParamsCheckbox . waitFor ( { state :"visible" } ) ;
1234-
1235- // If the checkbox is checked, uncheck it
1236- if ( await dynamicParamsCheckbox . isChecked ( ) ) {
1237- await dynamicParamsCheckbox . click ( ) ;
1238- }
1239-
1240- // Save the changes
1241- const saveButton = page . getByRole ( "button" , { name :/ s a v e / i} ) ;
1242- await saveButton . waitFor ( { state :"visible" } ) ;
1243- await saveButton . click ( ) ;
1244-
1245- // Wait for the success message or page to update
1246- await page
1247- . locator ( "[role='alert']:has-text('Template updated successfully')" )
1248- . first ( )
1249- . waitFor ( {
1250- state :"visible" ,
1251- timeout :15000 ,
1252- } ) ;
1253-
1254- // Additional wait to ensure the changes are persisted
1255- await page . waitForTimeout ( 500 ) ;
1256- } ;