-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathinstance-create.e2e.ts
More file actions
1330 lines (1059 loc) · 54.1 KB
/
instance-create.e2e.ts
File metadata and controls
1330 lines (1059 loc) · 54.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at https://mozilla.org/MPL/2.0/.
*
* Copyright Oxide Computer Company
*/
import { floatingIp } from '@oxide/api-mocks'
import {
closeToast,
expect,
expectNotVisible,
expectRowVisible,
expectVisible,
fillNumberInput,
selectOption,
test,
type Page,
} from './utils'
const selectASiloImage = async (page: Page, name: string) => {
await page.getByRole('tab', { name: 'Silo images' }).click()
await page.getByPlaceholder('Select a silo image', { exact: true }).click()
await page.getByRole('option', { name }).click()
}
const selectAProjectImage = async (page: Page, name: string) => {
await page.getByRole('tab', { name: 'Project images' }).click()
await page.getByPlaceholder('Select a project image', { exact: true }).click()
await page.getByRole('option', { name }).click()
}
const selectAnExistingDisk = async (page: Page, name: string) => {
await page.getByRole('tab', { name: 'Existing disks' }).click()
await page.getByRole('combobox', { name: 'Disk' }).click()
await page.getByRole('option', { name }).click()
}
test('can create an instance', async ({ page }) => {
await page.goto('/projects/mock-project/instances')
await page.locator('text="New Instance"').click()
await expect(page.getByRole('heading', { name: /Create instance/ })).toBeVisible()
await expect(page.getByRole('heading', { name: 'Hardware' })).toBeVisible()
await expect(page.getByRole('heading', { name: 'Boot disk' })).toBeVisible()
await expect(page.getByRole('heading', { name: 'Additional disks' })).toBeVisible()
await expect(page.getByRole('heading', { name: 'Networking' })).toBeVisible()
await expect(page.getByRole('textbox', { name: 'Description' })).toBeVisible()
await expect(page.getByRole('textbox', { name: 'Disk size (GiB)' })).toBeVisible()
const instanceName = 'my-instance'
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await page.fill('textarea[name=description]', 'An instance... from space!')
await page.locator('.ox-radio-card').nth(3).click()
await page.getByRole('textbox', { name: 'Disk name' }).fill('my-boot-disk')
const diskSizeInput = page.getByRole('textbox', { name: 'Disk size (GiB)' })
await diskSizeInput.fill('20')
// pick a project image just to show we can
await selectAProjectImage(page, 'image-3')
// hostname field should not exist
await expectNotVisible(page, ['role=textbox[name="Hostname"]'])
const v4Checkbox = page.getByRole('checkbox', {
name: 'Allocate IPv4 address',
})
const v6Checkbox = page.getByRole('checkbox', {
name: 'Allocate IPv6 address',
})
// verify that the IPv4 ephemeral IP checkbox is checked by default
await expect(v4Checkbox).toBeChecked()
await expect(v6Checkbox).toBeChecked()
// IPv4 default pool should be selected
const v4PoolDropdown = page.getByLabel('IPv4 pool')
await expect(v4PoolDropdown).toBeVisible()
await expect(v4PoolDropdown).toContainText('ip-pool-1')
// IPv6 default pool should be selected
const v6PoolDropdown = page.getByLabel('IPv6 pool')
await expect(v6PoolDropdown).toBeVisible()
await expect(v6PoolDropdown).toContainText('ip-pool-2')
await expect(page.getByRole('radiogroup', { name: 'Network interface' })).toBeVisible()
await expect(page.getByLabel('User data')).toBeVisible()
await page.getByRole('button', { name: 'Create instance' }).click()
await closeToast(page)
// instance goes from creating to starting to running as we poll
const pollingSpinner = page.getByLabel('Spinner')
await expect(pollingSpinner).toBeVisible()
await expect(page.getByText('Creating')).toBeVisible()
await expect(page.getByText('Starting')).toBeVisible()
await expect(page.getByText('Running')).toBeVisible()
await expect(pollingSpinner).toBeHidden()
// do this after state checks because sometimes it takes too long and we miss 'creating'
await expect(page).toHaveURL(`/projects/mock-project/instances/${instanceName}/storage`)
await expect(page.getByRole('heading', { name: instanceName })).toBeVisible()
await expect(page.getByText('16 vCPUs')).toBeVisible()
await expect(page.getByText('64 GiB')).toBeVisible()
await expect(page.getByText('from space')).toBeVisible()
// boot disk visible, no other disks attached
await expect(
page
.getByRole('table', { name: 'Boot disk' })
.getByRole('cell', { name: 'my-boot-disk' })
).toBeVisible()
await expect(page.getByText('No other disk')).toBeVisible()
// network tab works
await page.getByRole('tab', { name: 'Networking' }).click()
const table = page.getByRole('table', { name: 'Network interfaces' })
await expectRowVisible(table, {
name: 'defaultprimary',
vpc: 'mock-vpc',
subnet: 'mock-subnet',
})
})
test('ephemeral pool selection tracks network interface IP version', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
const v4Checkbox = page.getByRole('checkbox', {
name: 'Allocate IPv4 address',
})
const v6Checkbox = page.getByRole('checkbox', {
name: 'Allocate IPv6 address',
})
// Default NIC is dual-stack, both checkboxes should be visible, enabled, and checked
await expect(v4Checkbox).toBeVisible()
await expect(v4Checkbox).toBeEnabled()
await expect(v4Checkbox).toBeChecked()
await expect(v6Checkbox).toBeVisible()
await expect(v6Checkbox).toBeEnabled()
await expect(v6Checkbox).toBeChecked()
// Change to IPv6-only NIC - v4 checkbox should become disabled and unchecked
await selectOption(page, page.getByRole('button', { name: 'IPv4 & IPv6' }), 'IPv6')
await expect(v4Checkbox).toBeVisible()
await expect(v4Checkbox).toBeDisabled()
await expect(v4Checkbox).not.toBeChecked()
await expect(v6Checkbox).toBeVisible()
await expect(v6Checkbox).toBeEnabled()
await expect(v6Checkbox).toBeChecked()
// Verify disabled v4 checkbox shows tooltip
await v4Checkbox.hover()
await expect(page.getByText('Add an IPv4 network interface')).toBeVisible()
await expect(page.getByText('to attach an ephemeral IPv4 address')).toBeVisible()
// Change to IPv4-only NIC - v6 checkbox should become disabled and unchecked
await selectOption(page, page.getByRole('button', { name: 'IPv6', exact: true }), 'IPv4')
await expect(v4Checkbox).toBeVisible()
await expect(v4Checkbox).toBeEnabled()
await expect(v4Checkbox).toBeChecked()
await expect(v6Checkbox).toBeVisible()
await expect(v6Checkbox).toBeDisabled()
await expect(v6Checkbox).not.toBeChecked()
// Verify disabled v6 checkbox shows tooltip
await v6Checkbox.hover()
await expect(page.getByText('Add an IPv6 network interface')).toBeVisible()
await expect(page.getByText('to attach an ephemeral IPv6 address')).toBeVisible()
})
test('duplicate instance name produces visible error', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
await page.fill('input[name=name]', 'db1')
await selectAProjectImage(page, 'image-1')
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page.getByText('Instance name already exists')).toBeVisible()
})
test('first preset is auto-selected in each tab', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
await expect(page.getByRole('radio', { name: '2 CPU 8 gibibytes RAM' })).toBeChecked()
await page.getByRole('tab', { name: 'High CPU' }).click()
await expect(page.getByRole('radio', { name: '2 CPU 4 gibibytes RAM' })).toBeChecked()
await page.getByRole('tab', { name: 'High Memory' }).click()
await expect(page.getByRole('radio', { name: '2 CPU 16 gibibytes RAM' })).toBeChecked()
await page.getByRole('tab', { name: 'General Purpose' }).click()
await expect(page.getByRole('radio', { name: '2 CPU 8 gibibytes RAM' })).toBeChecked()
})
test('can create an instance with custom hardware', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
const instanceName = 'my-custom-instance'
await page.fill('input[name=name]', instanceName)
await page.fill('textarea[name=description]', 'An instance... from space!')
// Click the other tabs to make sure the custom input works
// even when something has been previously selected
await page.getByRole('tab', { name: 'High CPU' }).click()
await page.getByRole('tab', { name: 'High Memory' }).click()
await page.getByText('64 GiB RAM').click()
// Fill in custom specs
await page.getByRole('tab', { name: 'Custom' }).click()
await page.getByRole('textbox', { name: 'CPUs' }).fill('29')
await page.getByRole('textbox', { name: 'Memory (GiB)' }).fill('53')
await page.getByRole('textbox', { name: 'Disk name' }).fill('my-boot-disk')
const diskSizeInput = page.getByRole('textbox', { name: 'Disk size (GiB)' })
await diskSizeInput.fill('20')
await page.keyboard.press('Tab')
// pick a project image just to show we can
await selectAProjectImage(page, 'image-3')
// the disk size should bot have been changed from what was entered earlier
await expect(diskSizeInput).toHaveValue('20')
// test disk size validation against image size
// the minimum on the number input will be the size of the image (6GiB),
// so manually entering a number less than that will be corrected
await diskSizeInput.fill('5')
await page.keyboard.press('Tab')
await expect(diskSizeInput).toHaveValue('6')
const submitButton = page.getByRole('button', { name: 'Create instance' })
await submitButton.click() // submit to trigger validation
await expect(page).toHaveURL(`/projects/mock-project/instances/${instanceName}/storage`)
await expect(page.getByRole('heading', { name: instanceName })).toBeVisible()
await expect(page.getByText('29 vCPUs')).toBeVisible()
await expect(page.getByText('53 GiB')).toBeVisible()
await expect(page.getByText('from space')).toBeVisible()
})
test('automatically updates disk size when larger image selected', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
const instanceName = 'my-new-instance'
await page.fill('input[name=name]', instanceName)
// set the disk size larger than it needs to be, to verify it doesn't get reduced
const diskSizeInput = page.getByRole('textbox', { name: 'Disk size (GiB)' })
await diskSizeInput.fill('5')
await page.keyboard.press('Tab')
// pick a disk image that's smaller than 5GiB (the first project image works [4GiB])
await selectAProjectImage(page, 'image-1')
// test that it still says 5, as that's larger than the given image
await expect(diskSizeInput).toHaveValue('5')
// pick a disk image that's larger than 5GiB (the third project image works [6GiB])
await selectAProjectImage(page, 'image-3')
// test that it has been automatically increased to next-largest incremement of 10
await expect(diskSizeInput).toHaveValue('10')
// pick another image, just to verify that the diskSizeInput stays as it was
await selectAProjectImage(page, 'image-2')
await expect(diskSizeInput).toHaveValue('10')
const submitButton = page.getByRole('button', { name: 'Create instance' })
await submitButton.click()
await expect(page).toHaveURL(`/projects/mock-project/instances/${instanceName}/storage`)
await expectVisible(page, [`h1:has-text("${instanceName}")`, 'text=10 GiB'])
})
test('with disk name already taken', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
await page.fill('input[name=name]', 'my-instance')
await selectAProjectImage(page, 'image-1')
await page.fill('input[name=bootDiskName]', 'disk-1')
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page.getByText('Name is already in use').first()).toBeVisible()
})
test('can’t create a disk with a name that collides with the boot disk name', async ({
page,
}) => {
// Set up the instance and name the boot disk "disk-11"
await page.goto('/projects/mock-project/instances-new')
await page.fill('input[name=name]', 'another-instance')
await selectAProjectImage(page, 'image-1')
await page.fill('input[name=bootDiskName]', 'disk-11')
// Attempt to create a disk with the same name
await expect(page.getByText('No disks')).toBeVisible()
await page.getByRole('button', { name: 'Create new disk' }).click()
const dialog = page.getByRole('dialog')
await dialog.getByRole('textbox', { name: 'name' }).fill('disk-11')
await dialog.getByRole('button', { name: 'Create disk' }).click()
// Expect to see an error message
await expect(dialog.getByText('Name is already in use')).toBeVisible()
// Change the disk name to something else
await dialog.getByRole('textbox', { name: 'name' }).fill('disk-12')
await dialog.getByRole('button', { name: 'Create disk' }).click()
// The disk has been "created" (is in the list of Additional Disks)
await expectVisible(page, ['text=disk-12'])
await expect(page.getByText('No disks')).toBeHidden()
// Create the instance
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page).toHaveURL('/projects/mock-project/instances/another-instance/storage')
// Find the Boot Disk table and verify that disk-11 is there
const bootDiskTable = page.getByRole('table', { name: 'Boot disk' })
await expect(bootDiskTable.getByRole('cell', { name: 'disk-11' })).toBeVisible()
// Find the Other Disks table and verify that disk-12 is there
const otherDisksTable = page.getByRole('table', { name: 'Additional disks' })
await expect(otherDisksTable.getByRole('cell', { name: 'disk-12' })).toBeVisible()
})
test('add ssh key from instance create form', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
await expect(page.getByRole('checkbox', { name: 'm1-macbook-pro' })).toBeChecked()
await expect(page.getByRole('checkbox', { name: 'mac-mini' })).toBeChecked()
const newKey = 'new-key'
const newCheckbox = page.getByRole('checkbox', { name: newKey })
await expect(newCheckbox).toBeHidden()
// open model, fill form, and submit
const dialog = page.getByRole('dialog')
await page.getByRole('button', { name: 'Add SSH Key' }).click()
await dialog.getByRole('textbox', { name: 'Name' }).fill(newKey)
await dialog.getByRole('textbox', { name: 'Description' }).fill('hi')
await dialog.getByRole('textbox', { name: 'Public key' }).fill('some stuff, whatever')
await dialog.getByRole('button', { name: 'Add SSH Key' }).click()
await expect(newCheckbox).toBeVisible()
await expect(newCheckbox).toBeChecked()
await closeToast(page)
// pop over to the real SSH keys page and see it there, why not
await page.getByLabel('User menu').click()
await page.getByRole('menuitem', { name: 'Settings' }).click()
// the new key being auto-checked makes the form dirty, which triggers confirm leave
await page.getByRole('button', { name: 'Leave this page' }).click()
await page.getByRole('link', { name: 'SSH Keys' }).click()
await expectRowVisible(page.getByRole('table'), { name: newKey, description: 'hi' })
})
test('shows object not found error on no default pool', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('no-default-pool')
await selectAProjectImage(page, 'image-1')
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page.getByText('Not found: default IP pool for current silo')).toBeVisible()
})
test('create instance with existing disk', async ({ page }) => {
const instanceName = 'my-existing-disk-instance'
await page.goto('/projects/mock-project/instances-new')
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectAnExistingDisk(page, 'disk-3')
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page).toHaveURL(`/projects/mock-project/instances/${instanceName}/storage`)
await expectVisible(page, [`h1:has-text("${instanceName}")`, 'text=8 GiB'])
const bootDisks = page.getByRole('table', { name: 'Boot disk' })
await expectRowVisible(bootDisks, { Disk: 'disk-3' })
await expect(page.getByText('No other disks')).toBeVisible()
})
test('create instance with a silo image', async ({ page }) => {
const instanceName = 'my-existing-disk-2'
await page.goto('/projects/mock-project/instances-new')
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page).toHaveURL(`/projects/mock-project/instances/${instanceName}/storage`)
await expectVisible(page, [`h1:has-text("${instanceName}")`, 'text=10 GiB'])
})
test('start with an existing disk, but then switch to a silo image', async ({ page }) => {
const instanceName = 'silo-image-instance'
await page.goto('/projects/mock-project/instances-new')
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectAnExistingDisk(page, 'disk-7')
await selectASiloImage(page, 'ubuntu-22-04')
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page).toHaveURL(`/projects/mock-project/instances/${instanceName}/storage`)
await expectVisible(page, [`h1:has-text("${instanceName}")`, 'text=8 GiB'])
await expectNotVisible(page, ['text=disk-7'])
})
test('additional disks do not list committed disks as available', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
const attachExistingDiskButton = page.getByRole('button', {
name: 'Attach existing disk',
})
const selectADisk = page.getByPlaceholder('Select a disk')
const disk2 = page.getByRole('option', { name: 'disk-2' })
const disk3 = page.getByRole('option', { name: 'disk-3' })
const disk4 = page.getByRole('option', { name: 'disk-4' })
await attachExistingDiskButton.click()
await selectADisk.click()
// disk-2 is already attached, so should not be visible in the list
await expect(disk2).toBeHidden()
// disk-3, though, should be present
await expect(disk3).toBeVisible()
await expect(disk4).toBeVisible()
// select disk-3 and "attach" it to the instance that will be created
await disk3.click()
await page.getByRole('button', { name: 'Attach disk' }).click()
await attachExistingDiskButton.click()
await selectADisk.click()
// disk-2 should still be hidden
await expect(disk2).toBeHidden()
// now disk-3 should be hidden as well
await expect(disk3).toBeHidden()
await expect(disk4).toBeVisible()
})
test('maintains selected values even when changing tabs', async ({ page }) => {
const instanceName = 'arch-based-instance'
const arch = 'arch-2022-06-01'
await page.goto('/projects/mock-project/instances-new')
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
const imageSelectCombobox = page.getByRole('combobox', { name: 'Image' })
await imageSelectCombobox.scrollIntoViewIfNeeded()
// Filter the combobox for a particular silo image
await imageSelectCombobox.fill('arch')
// select the image
await page.getByRole('option', { name: arch }).click()
// expect to find name of the image on page
await expect(imageSelectCombobox).toHaveValue(arch)
// change to a different tab
await page.getByRole('tab', { name: 'Existing disks' }).click()
// the image should no longer be visible
await expect(imageSelectCombobox).toBeHidden()
// change back to the tab with the image
await page.getByRole('tab', { name: 'Silo images' }).click()
// arch should still be selected
await expect(imageSelectCombobox).toHaveValue(arch)
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page).toHaveURL(`/projects/mock-project/instances/${instanceName}/storage`)
await expectVisible(page, [`h1:has-text("${instanceName}")`, 'text=8 GiB'])
// when a disk name isn’t assigned, the generated one uses the name of the image,
// so this checks to make sure that the arch-based image — with name `arch-2022-06-01` — was used
await expectVisible(page, [`text=${instanceName}-${arch}`])
})
test('does not attach an ephemeral IP when the checkbox is unchecked', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('no-ephemeral-ip')
await selectAProjectImage(page, 'image-1')
// Uncheck both ephemeral IP checkboxes
await page.getByRole('checkbox', { name: 'Allocate IPv4 address' }).uncheck()
await page.getByRole('checkbox', { name: 'Allocate IPv6 address' }).uncheck()
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page).toHaveURL('/projects/mock-project/instances/no-ephemeral-ip/storage')
await expect(page.getByText('External IPs—')).toBeVisible()
})
test('attaches a floating IP; disables button when no IPs available', async ({ page }) => {
const attachFloatingIpButton = page.getByRole('button', { name: 'Attach floating IP' })
const dialog = page.getByRole('dialog')
const selectFloatingIpButton = dialog.getByRole('button', { name: 'Floating IP' })
const rootbeerFloatOption = page.getByRole('option', { name: 'rootbeer-float' })
const attachButton = dialog.getByRole('button', { name: 'Attach', exact: true })
const instanceName = 'with-floating-ip'
await page.goto('/projects/mock-project/instances-new')
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectAProjectImage(page, 'image-1')
await attachFloatingIpButton.click()
await expect(
page.getByText('This instance will be reachable at the selected IP')
).toBeVisible()
await selectFloatingIpButton.click()
await rootbeerFloatOption.click()
await expect(
page.getByText('This instance will be reachable at 123.4.56.4')
).toBeVisible()
await attachButton.click()
await expect(page.getByText('This instance will be reachable at')).toBeHidden()
await expectRowVisible(page.getByRole('table'), {
Name: floatingIp.name,
IP: floatingIp.ip,
})
// The button should still be enabled because there's still ipv6-float available
await expect(attachFloatingIpButton).toBeEnabled()
// Attach the IPv6 floating IP too
await attachFloatingIpButton.click()
await selectFloatingIpButton.click()
await page.getByRole('option', { name: 'ipv6-float' }).click()
await attachButton.click()
// Now the button should be disabled because both floating IPs are attached
await expect(attachFloatingIpButton).toBeDisabled()
// removing one floating IP row should work, and should re-enable the "attach" button
await page.getByRole('button', { name: 'remove floating IP rootbeer-float' }).click()
await expect(page.getByText(floatingIp.name)).toBeHidden()
await expect(attachFloatingIpButton).toBeEnabled()
// Remove the IPv6 floating IP too
await page.getByRole('button', { name: 'remove floating IP ipv6-float' }).click()
// re-attach the floating IP
await attachFloatingIpButton.click()
await selectFloatingIpButton.click()
await rootbeerFloatOption.click()
await attachButton.click()
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page).toHaveURL(`/projects/mock-project/instances/${instanceName}/storage`)
await expectVisible(page, [`h1:has-text("${instanceName}")`])
await page.getByRole('tab', { name: 'Networking' }).click()
// ensure External IPs table has rows for the Ephemeral IP and the Floating IP
const ipsTable = page.getByRole('table', { name: 'External IPs' })
await expectRowVisible(ipsTable, {
ip: '123.4.56.0',
Kind: 'ephemeral',
name: '—',
})
await expectRowVisible(ipsTable, {
ip: floatingIp.ip,
Kind: 'floating',
name: floatingIp.name,
})
})
test('attach a floating IP section has Empty version when no floating IPs exist on the project', async ({
page,
}) => {
await page.goto('/projects/other-project/instances-new')
await expect(page.getByRole('button', { name: 'Attach floating IP' })).toBeHidden()
await expect(
page.getByText('Create a floating IP to attach it to this instance')
).toBeVisible()
})
test('attaching additional disks allows for combobox filtering', async ({ page }) => {
await page.goto('/projects/other-project/instances-new')
const attachExistingDiskButton = page.getByRole('button', {
name: 'Attach existing disk',
})
const selectADisk = page.getByPlaceholder('Select a disk')
await attachExistingDiskButton.click()
await selectADisk.click()
// several disks should be shown
await expect(page.getByRole('option', { name: 'disk-0005' })).toBeVisible()
await expect(page.getByRole('option', { name: 'disk-0007' })).toBeVisible()
await expect(page.getByRole('option', { name: 'disk-0988' })).toBeVisible()
// type in a string to use as a filter
await selectADisk.fill('disk-02')
// only disks with that substring should be shown
await expect(page.getByRole('option', { name: 'disk-0023' })).toBeVisible()
await expect(page.getByRole('option', { name: 'disk-0125' })).toBeVisible()
await expect(page.getByRole('option', { name: 'disk-0211' })).toBeVisible()
await expect(page.getByRole('option', { name: 'disk-0220' })).toBeHidden()
await expect(page.getByRole('option', { name: 'disk-1000' })).toBeHidden()
// select one
await page.getByRole('option', { name: 'disk-0211' }).click()
// now options hidden and only the selected one is visible in the button/input
await expect(page.getByRole('option')).toBeHidden()
await expect(page.getByRole('combobox', { name: 'Disk name' })).toHaveValue('disk-0211')
// a random string should give a disabled option
await selectADisk.click()
await selectADisk.fill('asdf')
await expect(page.getByRole('option', { name: 'No items match' })).toBeVisible()
})
test('create instance with additional disks', async ({ page }) => {
const instanceName = 'more-disks'
await page.goto('/projects/mock-project/instances-new')
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectAProjectImage(page, 'image-1')
// Create a new disk
await page.getByRole('button', { name: 'Create new disk' }).click()
const createForm = page.getByRole('dialog', { name: 'Create disk' })
await expect(createForm).toBeVisible() // kill time to help size field flake?
// verify that an existing name can't be used
await createForm.getByRole('textbox', { name: 'Name', exact: true }).fill('disk-6')
const sizeField = createForm.getByRole('textbox', { name: 'Size (GiB)' })
// The size field can be overwritten by late renders in the parent form.
await fillNumberInput(sizeField, '5')
await createForm.getByRole('button', { name: 'Create disk' }).click()
await expect(createForm.getByText('Name is already in use')).toBeVisible()
// rename the disk to one that's allowed
await createForm.getByRole('textbox', { name: 'Name', exact: true }).fill('new-disk-1')
await createForm.getByRole('button', { name: 'Create disk' }).click()
const disksTable = page.getByRole('table', { name: 'Disks' })
await expect(disksTable.getByText('disk-6')).toBeHidden()
await expectRowVisible(disksTable, {
Name: 'new-disk-1',
Action: 'create',
Type: 'distributed',
Size: '5 GiB',
})
// Create a local disk
await page.getByRole('button', { name: 'Create new disk' }).click()
await createForm
.getByRole('textbox', { name: 'Name', exact: true })
.fill('new-disk-local')
await createForm.getByRole('textbox', { name: 'Size (GiB)' }).fill('10')
await createForm.getByRole('radio', { name: 'Local' }).click()
await createForm.getByRole('button', { name: 'Create disk' }).click()
await expectRowVisible(disksTable, {
Name: 'new-disk-local',
Action: 'create',
Type: 'local',
Size: '10 GiB',
})
// now that name is taken too, so disk create disallows it
await page.getByRole('button', { name: 'Create new disk' }).click()
await createForm.getByRole('textbox', { name: 'Name', exact: true }).fill('new-disk-1')
await createForm.getByRole('button', { name: 'Create disk' }).click()
await expect(createForm.getByText('Name is already in use')).toBeVisible()
await createForm.getByRole('button', { name: 'Cancel' }).click()
// Attach an existing disk
await page.getByRole('button', { name: 'Attach existing disk' }).click()
await selectOption(page, 'Disk name', 'disk-3')
await page.getByRole('button', { name: 'Attach disk' }).click()
await expectRowVisible(disksTable, {
Name: 'disk-3',
Action: 'attach',
Type: 'distributed',
Size: '6 GiB',
})
// Create the instance
await page.getByRole('button', { name: 'Create instance' }).click()
// Assert we're on the new instance's storage page
await expect(page).toHaveURL(`/projects/mock-project/instances/${instanceName}/storage`)
// Check for the boot disk
const bootDiskTable = page.getByRole('table', { name: 'Boot disk' })
// name is generated so it's gnarly
await expect(bootDiskTable.getByRole('cell', { name: /^more-disks-/ })).toBeVisible()
// Check for the additional disks
const otherDisksTable = page.getByRole('table', { name: 'Additional disks' })
await expectRowVisible(otherDisksTable, { Disk: 'new-disk-1', size: '5 GiB' })
await expectRowVisible(otherDisksTable, { Disk: 'new-disk-local', size: '10 GiB' })
await expectRowVisible(otherDisksTable, { Disk: 'disk-3', size: '6 GiB' })
})
test('Validate CPU and RAM', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
await page.getByRole('textbox', { name: 'Name', exact: true }).fill('db2')
await selectASiloImage(page, 'ubuntu-22-04')
await page.getByRole('tab', { name: 'Custom' }).click()
const cpu = page.getByRole('textbox', { name: 'CPU' })
await cpu.fill('999')
// blur CPU
const memory = page.getByRole('textbox', { name: 'Memory' })
await memory.click()
// make sure it's not clamping the value
await expect(cpu).toHaveValue('999')
await memory.fill('1537')
const submitButton = page.getByRole('button', { name: 'Create instance' })
const cpuMsg = page.getByText('Can be at most 254').first()
const memMsg = page.getByText('Can be at most 1536 GiB').first()
await expect(cpuMsg).toBeHidden()
await expect(memMsg).toBeHidden()
await submitButton.click()
await expect(cpuMsg).toBeVisible()
await expect(memMsg).toBeVisible()
})
test('create instance with IPv6-only networking', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
const instanceName = 'ipv6-only-instance'
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
// Configure networking
// Ensure "Default" network interface is selected
const defaultRadio = page.getByRole('radio', { name: 'Default', exact: true })
if (!(await defaultRadio.isChecked())) {
await defaultRadio.click()
}
// Wait for and select from the IP version dropdown
const ipVersionButton = page.locator('[name="defaultIpVersion"]')
await ipVersionButton.waitFor({ state: 'visible' })
await ipVersionButton.click()
await page.getByRole('option', { name: 'IPv6', exact: true }).click()
// Create instance
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page).toHaveURL(/\/instances\/ipv6-only-instance/)
// Navigate to the Networking tab
await page.getByRole('tab', { name: 'Networking' }).click()
// Check that the network interfaces table shows up
const nicTable = page.getByRole('table', { name: 'Network interfaces' })
await expect(nicTable).toBeVisible()
// Verify the Private IP column exists and contains an IPv6 address
const privateIpCell = nicTable.getByRole('cell').filter({ hasText: /::/ })
await expect(privateIpCell.first()).toBeVisible()
// Verify no IPv4 address is shown (no periods in a dotted-decimal format within the Private IP)
// We check that the cell with IPv6 doesn't also contain IPv4
const cellText = await privateIpCell.first().textContent()
expect(cellText).toMatch(/::/)
expect(cellText).not.toMatch(/\d+\.\d+\.\d+\.\d+/)
})
test('create instance with IPv4-only networking', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
const instanceName = 'ipv4-only-instance'
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
// Configure networking
// Ensure "Default" network interface is selected
const defaultRadio = page.getByRole('radio', { name: 'Default', exact: true })
if (!(await defaultRadio.isChecked())) {
await defaultRadio.click()
}
// Wait for and select from the IP version dropdown
const ipVersionButton = page.locator('[name="defaultIpVersion"]')
await ipVersionButton.waitFor({ state: 'visible' })
await ipVersionButton.click()
await page.getByRole('option', { name: 'IPv4', exact: true }).click()
// Create instance
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page).toHaveURL(/\/instances\/ipv4-only-instance/)
// Navigate to the Networking tab
await page.getByRole('tab', { name: 'Networking' }).click()
// Check that the network interfaces table shows up
const nicTable = page.getByRole('table', { name: 'Network interfaces' })
await expect(nicTable).toBeVisible()
// Verify the Private IP column exists and contains an IPv4 address
const privateIpCell = nicTable.getByRole('cell').filter({ hasText: /127\.0\.0\.1/ })
await expect(privateIpCell.first()).toBeVisible()
// Verify no IPv6 address is shown (no colons in IPv6 format within the Private IP)
const cellText = await privateIpCell.first().textContent()
expect(cellText).toMatch(/\d+\.\d+\.\d+\.\d+/)
expect(cellText).not.toMatch(/::/)
})
test('create instance with dual-stack networking shows both IPs', async ({ page }) => {
await page.goto('/projects/mock-project/instances-new')
const instanceName = 'dual-stack-instance'
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
// Configure networking
// Default is already "Default IPv4 & IPv6", so no need to select it
// Create instance
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page).toHaveURL(/\/instances\/dual-stack-instance/)
// Navigate to the Networking tab
await page.getByRole('tab', { name: 'Networking' }).click()
// Check that the network interfaces table shows up
const nicTable = page.getByRole('table', { name: 'Network interfaces' })
await expect(nicTable).toBeVisible()
// Verify both IPv4 and IPv6 addresses are shown
const privateIpCells = nicTable
.locator('tbody tr')
.first()
.locator('td')
.filter({ hasText: /127\.0\.0\.1/ })
await expect(privateIpCells.first()).toBeVisible()
// Check that the same cell contains IPv6
const cellText = await privateIpCells.first().textContent()
expect(cellText).toMatch(/127\.0\.0\.1/) // IPv4
expect(cellText).toMatch(/::1/) // IPv6
})
test('create instance with custom IPv4-only NIC constrains ephemeral IP to IPv4', async ({
page,
}) => {
await page.goto('/projects/mock-project/instances-new')
const instanceName = 'custom-ipv4-nic-test'
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
// Configure networking
// Select "Custom" network interface (use exact match and first to disambiguate from "custom pool")
await page.getByRole('radio', { name: 'Custom', exact: true }).first().click()
// Add a custom NIC with IPv4-only configuration
await page.getByRole('button', { name: 'Add network interface' }).click()
const modal = page.getByRole('dialog', { name: 'Add network interface' })
await expect(modal).toBeVisible()
await modal.getByRole('textbox', { name: 'Name' }).fill('my-ipv4-nic')
await modal.getByLabel('VPC', { exact: true }).click()
await page.getByRole('option', { name: 'mock-vpc' }).click()
await modal.getByLabel('Subnet').click()
await page.getByRole('option', { name: 'mock-subnet', exact: true }).click()
// Select IPv4-only IP configuration
await modal.getByRole('radio', { name: 'IPv4', exact: true }).click()
await modal.getByRole('button', { name: 'Add network interface' }).click()
await expect(modal).toBeHidden()
// Verify the NIC was added
const nicTable = page.getByRole('table', { name: 'Network Interfaces' })
await expect(
nicTable.getByRole('cell', { name: 'my-ipv4-nic', exact: true })
).toBeVisible()
// Verify that only IPv4 ephemeral IP is enabled
const v4Checkbox = page.getByRole('checkbox', {
name: 'Allocate IPv4 address',
})
const v6Checkbox = page.getByRole('checkbox', {
name: 'Allocate IPv6 address',
})
await expect(v4Checkbox).toBeVisible()
await expect(v4Checkbox).toBeEnabled()
await expect(v4Checkbox).toBeChecked()
await expect(v6Checkbox).toBeVisible()
await expect(v6Checkbox).toBeDisabled()
// IPv4 pool dropdown should be visible with default selected
const v4PoolDropdown = page.getByLabel('IPv4 pool')
await expect(v4PoolDropdown).toBeVisible()
await expect(v4PoolDropdown).toContainText('ip-pool-1')
// Open dropdown to check available options - only IPv4 pools should appear
await v4PoolDropdown.click()
await expect(page.getByRole('option', { name: 'ip-pool-1' })).toBeVisible()
// Close dropdown to avoid obscuring subsequent interactions
await page.keyboard.press('Escape')
// Create the instance
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page).toHaveURL(`/projects/mock-project/instances/${instanceName}/storage`)
// Verify exactly one ephemeral IP row exists, and it is IPv4
await page.getByRole('tab', { name: 'Networking' }).click()
const externalIpsTable = page.getByRole('table', { name: /external ips/i })
const ephemeralRows = externalIpsTable.getByRole('row').filter({ hasText: 'ephemeral' })
await expect(ephemeralRows).toHaveCount(1)
await expect(externalIpsTable.getByText('v4')).toBeVisible()
await expect(externalIpsTable.getByText('v6')).toBeHidden()
})
test('create instance with custom IPv6-only NIC constrains ephemeral IP to IPv6', async ({
page,
}) => {
await page.goto('/projects/mock-project/instances-new')
const instanceName = 'custom-ipv6-nic-test'
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')
// Configure networking
// Select "Custom" network interface (use exact match and first to disambiguate from "custom pool")
await page.getByRole('radio', { name: 'Custom', exact: true }).first().click()
// Add a custom NIC with IPv6-only configuration
await page.getByRole('button', { name: 'Add network interface' }).click()
const modal = page.getByRole('dialog', { name: 'Add network interface' })
await expect(modal).toBeVisible()
await modal.getByRole('textbox', { name: 'Name' }).fill('my-ipv6-nic')
await modal.getByLabel('VPC', { exact: true }).click()
await page.getByRole('option', { name: 'mock-vpc' }).click()
await modal.getByLabel('Subnet').click()
await page.getByRole('option', { name: 'mock-subnet', exact: true }).click()
// Select IPv6-only IP configuration
await modal.getByRole('radio', { name: 'IPv6', exact: true }).click()
await modal.getByRole('button', { name: 'Add network interface' }).click()
await expect(modal).toBeHidden()
// Verify the NIC was added
const nicTable = page.getByRole('table', { name: 'Network Interfaces' })
await expect(
nicTable.getByRole('cell', { name: 'my-ipv6-nic', exact: true })
).toBeVisible()
// Verify that only IPv6 ephemeral IP is enabled
const v4Checkbox = page.getByRole('checkbox', {
name: 'Allocate IPv4 address',
})
const v6Checkbox = page.getByRole('checkbox', {
name: 'Allocate IPv6 address',
})
await expect(v4Checkbox).toBeVisible()
await expect(v4Checkbox).toBeDisabled()
await expect(v6Checkbox).toBeVisible()
await expect(v6Checkbox).toBeEnabled()
await expect(v6Checkbox).toBeChecked()
// IPv6 pool dropdown should be visible with default selected
const v6PoolDropdown = page.getByLabel('IPv6 pool')
await expect(v6PoolDropdown).toBeVisible()
await expect(v6PoolDropdown).toContainText('ip-pool-2')
// Open dropdown to check available options - only IPv6 pools should appear
await v6PoolDropdown.click()
await expect(page.getByRole('option', { name: 'ip-pool-2' })).toBeVisible()
// Close dropdown to avoid obscuring subsequent interactions
await page.keyboard.press('Escape')
// Create the instance
await page.getByRole('button', { name: 'Create instance' }).click()
await expect(page).toHaveURL(`/projects/mock-project/instances/${instanceName}/storage`)
// Verify exactly one ephemeral IP row exists, and it is IPv6
await page.getByRole('tab', { name: 'Networking' }).click()
const externalIpsTable = page.getByRole('table', { name: /external ips/i })
const ephemeralRows = externalIpsTable.getByRole('row').filter({ hasText: 'ephemeral' })
await expect(ephemeralRows).toHaveCount(1)
await expect(externalIpsTable.getByText('v4')).toBeHidden()
await expect(externalIpsTable.getByText('v6')).toBeVisible()
})
test('create instance with custom dual-stack NIC allows both IPv4 and IPv6 ephemeral IPs', async ({
page,
}) => {
await page.goto('/projects/mock-project/instances-new')
const instanceName = 'custom-dual-stack-nic-test'
await page.getByRole('textbox', { name: 'Name', exact: true }).fill(instanceName)
await selectASiloImage(page, 'ubuntu-22-04')