@@ -19,106 +19,216 @@ import { expect, test } from '../e2e/utils'
1919// set a fixed time to avoid diffs due to irrelevant time differences
2020test . beforeEach ( async ( { page } ) => {
2121 await page . clock . setFixedTime ( new Date ( '2025-10-23T12:34:56.000Z' ) )
22+ // TODO: revert to default viewport once we've confirmed no visual regressions
23+ // from the grid layout change. The tall viewport forces all content to render
24+ // without scrolling, so fullPage screenshots are comparable between the old
25+ // contained-scroll layout and the new document-scroll layout.
26+ await page . setViewportSize ( { width : 1280 , height : 3100 } )
2227} )
2328
24- test . describe ( 'Visual Regression' , { tag : '@visual' } , ( ) => {
25- test ( 'projects list' , async ( { page } ) => {
26- await page . goto ( '/projects' )
27- await expect ( page . getByRole ( 'heading' , { name : 'Projects' } ) ) . toBeVisible ( )
28- await expect ( page ) . toHaveScreenshot ( 'projects-list.png' , { fullPage : true } )
29- } )
30-
31- test ( 'instances list' , async ( { page } ) => {
32- await page . goto ( '/projects/mock-project/instances' )
33- await expect ( page . getByRole ( 'heading' , { name : 'Instances' } ) ) . toBeVisible ( )
34- await expect ( page ) . toHaveScreenshot ( 'instances-list.png' , { fullPage : true } )
35- } )
36-
37- test ( 'instance detail' , async ( { page } ) => {
38- await page . goto ( '/projects/mock-project/instances/db1' )
39- await expect ( page . getByRole ( 'heading' , { name : 'db1' } ) ) . toBeVisible ( )
40- await expect ( page ) . toHaveScreenshot ( 'instance-detail.png' , { fullPage : true } )
41- } )
42-
43- test ( 'create disk' , async ( { page } ) => {
44- await page . goto ( '/projects/mock-project/disks-new' )
45- await expect ( page . getByRole ( 'heading' , { name : 'Create disk' } ) ) . toBeVisible ( )
46- await expect ( page ) . toHaveScreenshot ( 'disks-new.png' , { fullPage : true } )
47- } )
29+ const fullPage = { fullPage : true }
30+
31+ const p = '/projects/mock-project'
32+
33+ // Standard pages: goto URL, wait for heading, take full-page screenshot
34+ const pages = [
35+ // Auth
36+ { name : 'device verify' , url : '/device/verify' , heading : 'Device Authentication' } ,
37+ { name : 'device success' , url : '/device/success' , heading : 'Device logged in' } ,
38+
39+ // Settings
40+ { name : 'settings profile' , url : '/settings/profile' , heading : 'Profile' } ,
41+ { name : 'settings SSH keys' , url : '/settings/ssh-keys' , heading : 'SSH Keys' } ,
42+ {
43+ name : 'settings access tokens' ,
44+ url : '/settings/access-tokens' ,
45+ heading : 'Access Tokens' ,
46+ } ,
47+
48+ // Silo
49+ { name : 'projects list' , url : '/projects' , heading : 'Projects' } ,
50+ {
51+ name : 'silo image edit' ,
52+ url : '/images/arch-2022-06-01/edit' ,
53+ heading : 'Silo image' ,
54+ exact : true ,
55+ } ,
56+ { name : 'silo utilization' , url : '/utilization' , heading : 'Utilization' } ,
57+ { name : 'silo access' , url : '/access' , heading : 'Silo Access' } ,
58+
59+ // Project - Instances
60+ { name : 'instances list' , url : `${ p } /instances` , heading : 'Instances' } ,
61+ { name : 'instance create' , url : `${ p } /instances-new` , heading : 'Create instance' } ,
62+ { name : 'instance storage tab' , url : `${ p } /instances/db1/storage` , heading : 'db1' } ,
63+ { name : 'instance networking tab' , url : `${ p } /instances/db1/networking` , heading : 'db1' } ,
64+ { name : 'instance metrics cpu' , url : `${ p } /instances/db1/metrics/cpu` , heading : 'db1' } ,
65+ { name : 'instance metrics disk' , url : `${ p } /instances/db1/metrics/disk` , heading : 'db1' } ,
66+ {
67+ name : 'instance metrics network' ,
68+ url : `${ p } /instances/db1/metrics/network` ,
69+ heading : 'db1' ,
70+ } ,
71+ { name : 'instance connect tab' , url : `${ p } /instances/db1/connect` , heading : 'db1' } ,
72+ { name : 'instance settings tab' , url : `${ p } /instances/db1/settings` , heading : 'db1' } ,
73+
74+ // Project - Disks
75+ { name : 'disks list' , url : `${ p } /disks` , heading : 'Disks' } ,
76+ { name : 'create disk' , url : `${ p } /disks-new` , heading : 'Create disk' } ,
77+
78+ // Project - Snapshots, Images
79+ { name : 'snapshots list' , url : `${ p } /snapshots` , heading : 'Snapshots' } ,
80+ { name : 'images list' , url : `${ p } /images` , heading : 'Images' } ,
81+ { name : 'image upload' , url : `${ p } /images-new` , heading : 'Upload image' } ,
82+
83+ // Project - VPCs
84+ { name : 'vpcs list' , url : `${ p } /vpcs` , heading : 'VPCs' } ,
85+ {
86+ name : 'vpc firewall rules' ,
87+ url : `${ p } /vpcs/mock-vpc/firewall-rules` ,
88+ heading : 'mock-vpc' ,
89+ } ,
90+ { name : 'vpc subnets' , url : `${ p } /vpcs/mock-vpc/subnets` , heading : 'mock-vpc' } ,
91+ { name : 'vpc routers' , url : `${ p } /vpcs/mock-vpc/routers` , heading : 'mock-vpc' } ,
92+ {
93+ name : 'vpc internet gateways' ,
94+ url : `${ p } /vpcs/mock-vpc/internet-gateways` ,
95+ heading : 'mock-vpc' ,
96+ } ,
97+ {
98+ name : 'vpc router detail' ,
99+ url : `${ p } /vpcs/mock-vpc/routers/mock-custom-router` ,
100+ heading : 'mock-custom-router' ,
101+ } ,
102+
103+ // Project - Networking
104+ { name : 'floating IPs' , url : `${ p } /floating-ips` , heading : 'Floating IPs' } ,
105+ { name : 'external subnets' , url : `${ p } /external-subnets` , heading : 'External Subnets' } ,
106+
107+ // Project - Other
108+ { name : 'project access' , url : `${ p } /access` , heading : 'Project Access' } ,
109+ { name : 'affinity groups' , url : `${ p } /affinity` , heading : 'Affinity Groups' } ,
110+ {
111+ name : 'anti-affinity group detail' ,
112+ url : `${ p } /affinity/romulus-remus` ,
113+ heading : 'romulus-remus' ,
114+ } ,
115+
116+ // System - Silos
117+ { name : 'system silos list' , url : '/system/silos' , heading : 'Silos' } ,
118+ { name : 'silo detail idps' , url : '/system/silos/maze-war/idps' , heading : 'maze-war' } ,
119+ {
120+ name : 'silo detail ip pools' ,
121+ url : '/system/silos/maze-war/ip-pools' ,
122+ heading : 'maze-war' ,
123+ } ,
124+ {
125+ name : 'silo detail subnet pools' ,
126+ url : '/system/silos/maze-war/subnet-pools' ,
127+ heading : 'maze-war' ,
128+ } ,
129+ { name : 'silo detail quotas' , url : '/system/silos/maze-war/quotas' , heading : 'maze-war' } ,
130+ {
131+ name : 'silo detail fleet roles' ,
132+ url : '/system/silos/maze-war/fleet-roles' ,
133+ heading : 'maze-war' ,
134+ } ,
135+ { name : 'silo detail scim' , url : '/system/silos/maze-war/scim' , heading : 'maze-war' } ,
136+
137+ // System - Utilization
138+ { name : 'system utilization' , url : '/system/utilization' , heading : 'Utilization' } ,
139+ {
140+ name : 'system utilization metrics tab' ,
141+ url : '/system/utilization?tab=metrics' ,
142+ heading : 'Utilization' ,
143+ } ,
144+
145+ // System - Networking
146+ { name : 'system ip pools' , url : '/system/networking/ip-pools' , heading : 'IP Pools' } ,
147+ {
148+ name : 'ip pool detail' ,
149+ url : '/system/networking/ip-pools/ip-pool-1' ,
150+ heading : 'ip-pool-1' ,
151+ } ,
152+ {
153+ name : 'ip pool silos tab' ,
154+ url : '/system/networking/ip-pools/ip-pool-1?tab=silos' ,
155+ heading : 'ip-pool-1' ,
156+ } ,
157+ {
158+ name : 'system subnet pools' ,
159+ url : '/system/networking/subnet-pools' ,
160+ heading : 'Subnet Pools' ,
161+ } ,
162+ {
163+ name : 'subnet pool detail' ,
164+ url : '/system/networking/subnet-pools/default-v4-subnet-pool' ,
165+ heading : 'default-v4-subnet-pool' ,
166+ } ,
167+ {
168+ name : 'subnet pool silos tab' ,
169+ url : '/system/networking/subnet-pools/default-v4-subnet-pool?tab=silos' ,
170+ heading : 'default-v4-subnet-pool' ,
171+ } ,
172+
173+ // System - Inventory
174+ { name : 'inventory sleds' , url : '/system/inventory/sleds' , heading : 'Inventory' } ,
175+ { name : 'inventory disks' , url : '/system/inventory/disks' , heading : 'Inventory' } ,
176+ {
177+ name : 'sled instances' ,
178+ url : '/system/inventory/sleds/c2519937-44a4-493b-9b38-5c337c597d08/instances' ,
179+ heading : 'Sled' ,
180+ } ,
181+
182+ // System - Update & Access
183+ { name : 'system update' , url : '/system/update' , heading : 'System Update' } ,
184+ { name : 'fleet access' , url : '/system/access' , heading : 'Fleet Access' } ,
185+
186+ // Error
187+ { name : 'not found' , url : '/nonexistent' , heading : 'Page not found' } ,
188+ ]
48189
49- test ( 'disks list' , async ( { page } ) => {
50- await page . goto ( '/projects/mock-project/disks' )
51- await expect ( page . getByRole ( 'heading' , { name : 'Disks' } ) ) . toBeVisible ( )
52- await expect ( page ) . toHaveScreenshot ( 'disks-list.png' , { fullPage : true } )
53- } )
190+ test . describe ( 'Visual Regression' , { tag : '@visual' } , ( ) => {
191+ for ( const { name, url, heading, exact } of pages ) {
192+ const screenshot = name . replaceAll ( ' ' , '-' ) + '.png'
193+ test ( name , async ( { page } ) => {
194+ await page . goto ( url , { waitUntil : 'networkidle' } )
195+ await expect ( page . getByRole ( 'heading' , { name : heading , exact } ) ) . toBeVisible ( )
196+ await expect ( page ) . toHaveScreenshot ( screenshot , fullPage )
197+ } )
198+ }
54199
55- test ( 'vpcs list' , async ( { page } ) => {
56- await page . goto ( '/projects/mock-project/vpcs' )
57- await expect ( page . getByRole ( 'heading' , { name : 'VPCs' } ) ) . toBeVisible ( )
58- await expect ( page ) . toHaveScreenshot ( 'vpcs-list.png' , { fullPage : true } )
59- } )
200+ // Special cases that don't fit the standard pattern
60201
61- test ( 'snapshots list' , async ( { page } ) => {
62- await page . goto ( '/projects/mock-project/snapshots' )
63- await expect ( page . getByRole ( 'heading' , { name : 'Snapshots' } ) ) . toBeVisible ( )
64- await expect ( page ) . toHaveScreenshot ( 'snapshots-list.png' , { fullPage : true } )
65- } )
66-
67- test ( 'images list' , async ( { page } ) => {
68- await page . goto ( '/projects/mock-project/images' )
69- await expect ( page . getByRole ( 'heading' , { name : 'Images' } ) ) . toBeVisible ( )
70- await expect ( page ) . toHaveScreenshot ( 'images-list.png' , { fullPage : true } )
202+ test ( 'login form' , async ( { page } ) => {
203+ await page . goto ( '/login/default-silo/local' , { waitUntil : 'networkidle' } )
204+ await expect ( page ) . toHaveURL ( / \/ l o g i n / )
205+ await expect ( page ) . toHaveScreenshot ( 'login-form.png' )
71206 } )
72207
73- test ( 'silo images list ' , async ( { page } ) => {
74- await page . goto ( '/images' )
208+ test ( 'silo images' , async ( { page } ) => {
209+ await page . goto ( '/images' , { waitUntil : 'networkidle' } )
75210 await expect ( page . getByRole ( 'heading' , { name : 'Silo Images' } ) ) . toBeVisible ( )
76- await expect ( page ) . toHaveScreenshot ( 'silo-images.png' , { fullPage : true } )
77- await page . click ( 'role=button[name="Promote image"]' )
78- await expect ( page ) . toHaveScreenshot ( 'silo-images-promote.png' , { fullPage : true } )
79- } )
80-
81- test ( 'silo image' , async ( { page } ) => {
82- await page . goto ( '/images/arch-2022-06-01/edit' )
83- await expect (
84- page . getByRole ( 'heading' , { name : 'Silo image' , exact : true } )
85- ) . toBeVisible ( )
86- await expect ( page ) . toHaveScreenshot ( 'silo-image.png' , { fullPage : true } )
87- } )
88-
89- test ( 'system utilization' , async ( { page } ) => {
90- await page . goto ( '/utilization' )
91- await expect ( page . getByRole ( 'heading' , { name : 'Utilization' } ) ) . toBeVisible ( )
92- await expect ( page ) . toHaveScreenshot ( 'system-utilization.png' , { fullPage : true } )
93- } )
94-
95- test ( 'system silos list' , async ( { page } ) => {
96- await page . goto ( '/system/silos' )
97- await expect ( page . getByRole ( 'heading' , { name : 'Silos' } ) ) . toBeVisible ( )
98- await expect ( page ) . toHaveScreenshot ( 'system-silos.png' , { fullPage : true } )
211+ await expect ( page ) . toHaveScreenshot ( 'silo-images.png' , fullPage )
212+ await page . getByRole ( 'button' , { name : 'Promote image' } ) . click ( )
213+ await page . waitForLoadState ( 'networkidle' )
214+ await expect ( page ) . toHaveScreenshot ( 'silo-images-promote.png' , fullPage )
99215 } )
100216
101- test ( 'system networking ip pools' , async ( { page } ) => {
102- await page . goto ( '/system/networking/ip-pools' )
103- await expect ( page . getByRole ( 'heading' , { name : 'IP Pools' } ) ) . toBeVisible ( )
104- await expect ( page ) . toHaveScreenshot ( 'system-ip-pools.png' , { fullPage : true } )
105- } )
106-
107- test ( 'settings profile' , async ( { page } ) => {
108- await page . goto ( '/settings/profile' )
109- await expect ( page . getByRole ( 'heading' , { name : 'Profile' } ) ) . toBeVisible ( )
110- await expect ( page ) . toHaveScreenshot ( 'settings-profile.png' , { fullPage : true } )
217+ test ( 'saml login' , async ( { page } ) => {
218+ await page . goto ( '/login/default-silo/saml/mock-idp' , { waitUntil : 'networkidle' } )
219+ await expect ( page ) . toHaveURL ( / \/ l o g i n / )
220+ await expect ( page ) . toHaveScreenshot ( 'saml-login.png' )
111221 } )
112222
113- test ( 'login form' , async ( { page } ) => {
114- await page . goto ( '/login/default-silo/local' )
115-
116- await expect ( page ) . toHaveURL ( / \/ l o g i n / )
117- await expect ( page ) . toHaveScreenshot ( 'login-form.png' )
223+ test ( 'serial console' , async ( { page } ) => {
224+ await page . goto ( `${ p } /instances/db1/serial-console` , { waitUntil : 'networkidle' } )
225+ await expect ( page . getByText ( 'Serial Console' ) ) . toBeVisible ( )
226+ await expect ( page ) . toHaveScreenshot ( 'serial-console.png' , fullPage )
118227 } )
119228
120229 test ( 'command menu' , async ( { page } ) => {
121230 await page . keyboard . press ( `ControlOrMeta+k` )
122- await expect ( page ) . toHaveScreenshot ( 'command-menu.png' , { fullPage : true } )
231+ await page . waitForLoadState ( 'networkidle' )
232+ await expect ( page ) . toHaveScreenshot ( 'command-menu.png' , fullPage )
123233 } )
124234} )
0 commit comments