Skip to content

Commit 901bfec

Browse files
committed
fix(FR-1844): migrate E2E tests to Ant Design 6 locators (#4919)
Resolves [FR-1844](https://lablup.atlassian.net/browse/FR-1844) ## Summary Migrate E2E test locators to Ant Design 6 patterns following the upgrade from Ant Design 5. This fixes broken E2E tests by replacing fragile class-based selectors with semantic, role-based selectors. ## Changes ### Modal Selectors Migration - `.ant-modal-content` → `getByRole('dialog')` - More semantic and resilient to DOM structure changes - Updated in: environment.test.ts, session.test.ts, vfolder-explorer-modal.spec.ts ### Dropdown & Combobox Updates - `.ant-select-dropdown` → `getByRole('option')` - Complex nested combobox locators → `getByRole('combobox')` - Simplified selectors in: session.test.ts ### Notification Selectors - `.ant-notification-notice-message` → `getByText()` - User-centric, content-based selector - Updated in: login.test.ts ### Input Number Unit Selector - Updated structure for Ant Design 6: `.ant-select .ant-typography` - Fixed in: environment.test.ts ### Test Utility Classes - FolderExplorerModal: Updated to use `getByRole('dialog')` - FolderCreationModal: Text-based card selectors - StartPage: Improved element detection ### Viewport Fixes - Added `scrollIntoViewIfNeeded()` for dropdown options - Prevents "Element is outside of the viewport" errors ## Benefits ✅ **Semantic selectors**: Role-based locators reflect actual user interactions ✅ **Better accessibility**: Tests verify accessible interface elements ✅ **Resilient to changes**: Not tied to CSS implementation details ✅ **Follows Playwright best practices**: Recommended selector patterns ## Test Coverage - ✅ agent.test.ts - ✅ config.test.ts - ✅ environment.test.ts - ✅ login.test.ts - ✅ maintenance.test.ts - ✅ session.test.ts - ✅ vfolder-explorer-modal.spec.ts **Files Changed:** 11 files (+117/-124) [FR-1844]: https://lablup.atlassian.net/browse/FR-1844?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 8d1523d commit 901bfec

11 files changed

Lines changed: 117 additions & 124 deletions

.cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"Keypairs",
4444
"Lablup",
4545
"lucide",
46+
"networkidle",
4647
"noti",
4748
"OPENBLAS",
4849
"Popconfirm",

e2e/agent.test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
import { loginAsAdmin } from './utils/test-util';
22
import { checkActiveTab, findColumnIndex } from './utils/test-util-antd';
3-
import { test, expect } from '@playwright/test';
3+
import { test, expect, Locator } from '@playwright/test';
44

55
test.beforeEach(async ({ page }) => {
66
await loginAsAdmin(page);
7+
await page.getByRole('menuitem', { name: 'Admin Settings' }).click();
78
await page.getByRole('menuitem', { name: 'hdd Resources' }).click();
89
await expect(
910
page.getByTestId('webui-breadcrumb').getByText('Resources'),
1011
).toBeVisible();
12+
await page.waitForLoadState('networkidle');
13+
// Click on Agent tab if not already active
14+
await page.getByRole('tab', { name: 'Agent' }).click();
1115
});
1216

1317
test.describe('Agent list', () => {
14-
let resourcesPageTab;
15-
let agentListTable;
18+
let agentListTable: Locator;
1619

1720
test.beforeEach(async ({ page }) => {
1821
const firstCard = await page
1922
.locator('.ant-layout-content .ant-card')
2023
.first();
21-
resourcesPageTab = await firstCard.locator('.ant-tabs');
2224
agentListTable = await firstCard.locator('.ant-table');
2325
});
2426

e2e/config.test.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,16 +71,17 @@ test.describe.parallel('config.toml', () => {
7171
await loginAsAdmin(page);
7272

7373
// Step 2: Go to Environments page and find an uninstalled image
74-
await page
75-
.getByRole('group')
76-
.getByText('Environments', { exact: true })
77-
.click();
78-
79-
// Wait for the table to load and have data (excluding measure rows)
80-
await page.waitForSelector('table tbody tr:not(.ant-table-measure-row)', {
81-
state: 'visible',
82-
timeout: 15000,
83-
});
74+
await page.getByRole('menuitem', { name: 'Admin Settings' }).click();
75+
await page.getByRole('menuitem', { name: 'Environments' }).click();
76+
77+
// Wait for the table to load and have data (excluding measure rows and placeholder)
78+
await page.waitForSelector(
79+
'table tbody tr:not(.ant-table-measure-row):not(.ant-table-placeholder)',
80+
{
81+
state: 'visible',
82+
timeout: 15000,
83+
},
84+
);
8485

8586
// Sort by Status column to get uninstalled images first
8687
const statusHeader = page.getByRole('columnheader', { name: 'Status' });
@@ -89,7 +90,9 @@ test.describe.parallel('config.toml', () => {
8990
// Find the first row where the status cell (2nd cell) is empty (uninstalled image)
9091
// Uninstalled images have no status badge
9192
// Exclude ant-table-measure-row which is hidden
92-
const allRows = page.locator('tbody tr:not(.ant-table-measure-row)');
93+
const allRows = page.locator(
94+
'tbody tr:not(.ant-table-measure-row):not(.ant-table-placeholder)',
95+
);
9396
let uninstalledImageName: string | null = null;
9497

9598
// Wait for at least one row to be available

e2e/environment.test.ts

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
import { loginAsAdmin, navigateTo } from './utils/test-util';
2-
import { findColumnIndex, getMenuItem } from './utils/test-util-antd';
2+
import { findColumnIndex } from './utils/test-util-antd';
33
import { expect, test } from '@playwright/test';
44

55
test.describe('environment ', () => {
66
test.beforeEach(async ({ page }) => {
77
await loginAsAdmin(page);
8-
await getMenuItem(page, 'Environments').click();
8+
await page.getByRole('menuitem', { name: 'Admin Settings' }).click();
9+
await page
10+
.getByRole('menuitem', { name: 'file-done Environments' })
11+
.click();
912
await expect(page).toHaveURL(/\/environment/);
1013
await page.waitForLoadState('networkidle');
14+
// Wait for the table to be visible
15+
await page
16+
.locator('.ant-table-content')
17+
.waitFor({ state: 'visible', timeout: 10000 });
1118
});
1219
test('Rendering Image List', async ({ page }) => {
1320
const table = page.locator('.ant-table-content');
@@ -74,9 +81,10 @@ test.describe('environment ', () => {
7481
.click();
7582
await page.waitForLoadState('networkidle');
7683
// get resource limit from control modal
77-
const resourceLimitControlModal = page.locator(
78-
'.ant-modal-content:has-text("Modify Minimum Image Resource Limit")',
79-
);
84+
const resourceLimitControlModal = page.getByRole('dialog', {
85+
name: /Modify Minimum Image Resource Limit/i,
86+
});
87+
8088
await expect(resourceLimitControlModal).toBeVisible();
8189

8290
const cpuFormItem = resourceLimitControlModal.locator(
@@ -92,8 +100,9 @@ test.describe('environment ', () => {
92100
'.ant-input-number input',
93101
);
94102
const memoryValue = await memoryFormItemInput.getAttribute('value');
103+
// In Ant Design 6, the unit selector structure changed - use .ant-select .ant-typography
95104
const memorySize = await memoryFormItem
96-
.locator('.ant-input-number-group-addon .ant-select-selection-item')
105+
.locator('.ant-select .ant-typography')
97106
.textContent();
98107
// modify resource limit
99108
await cpuFormItemInput.fill(CPU_CORE);
@@ -115,9 +124,10 @@ test.describe('environment ', () => {
115124
.getByRole('button', { name: 'setting' })
116125
.click();
117126
await page.waitForLoadState('networkidle');
118-
const modifiedResourceLimitControlModal = page.locator(
119-
'.ant-modal-content:has-text("Modify Minimum Image Resource Limit")',
120-
);
127+
// In Ant Design 6, use role-based selector for dialog
128+
const modifiedResourceLimitControlModal = page.getByRole('dialog', {
129+
name: /Modify Minimum Image Resource Limit/i,
130+
});
121131
await expect(modifiedResourceLimitControlModal).toBeVisible();
122132
const modifiedCpuFormItemInput = modifiedResourceLimitControlModal.locator(
123133
'.ant-form-item-row:has-text("CPU") input',
@@ -128,10 +138,11 @@ test.describe('environment ', () => {
128138
);
129139
await expect(modifiedCpuFormItemInput).toHaveValue(CPU_CORE);
130140
await expect(modifiedMemoryFormItemInput).toHaveValue(MEMORY_SIZE);
141+
// In Ant Design 6, the unit selector structure changed - use .ant-select .ant-typography
131142
await expect(
132-
memoryFormItem.locator(
133-
'.ant-input-number-group-addon .ant-select-selection-item',
134-
),
143+
modifiedResourceLimitControlModal
144+
.locator('.ant-form-item-row:has-text("Memory")')
145+
.locator('.ant-select .ant-typography'),
135146
).toHaveText('GiB');
136147

137148
// reset resource limit
@@ -141,8 +152,9 @@ test.describe('environment ', () => {
141152
await expect(modifiedMemoryFormItemInput).toHaveValue(
142153
memoryValue as string,
143154
);
155+
// In Ant Design 6, click on the select component wrapper
144156
const memorySizeAddon = modifiedResourceLimitControlModal.locator(
145-
'.ant-form-item-row:has-text("Memory") .ant-select-selector',
157+
'.ant-form-item-row:has-text("Memory") .ant-select',
146158
);
147159
await memorySizeAddon.click();
148160
await page
@@ -175,7 +187,8 @@ test.describe('environment ', () => {
175187
.click();
176188

177189
// Add app
178-
const modal = page.locator('.ant-modal-content:has-text("Manage Apps")');
190+
// In Ant Design 6, use role-based selector for dialog
191+
const modal = page.getByRole('dialog', { name: /Manage Apps/i });
179192
await expect(modal).toBeVisible();
180193
const numberOfAppsBeforeAdd = await modal.locator('.ant-form-item').count();
181194
await modal.getByRole('button', { name: 'plus Add' }).click();
@@ -207,9 +220,8 @@ test.describe('environment ', () => {
207220
.nth(controlColumnIndex)
208221
.getByRole('button', { name: 'appstore' })
209222
.click();
210-
const modalAfterAdd = page.locator(
211-
'.ant-modal-content:has-text("Manage Apps")',
212-
);
223+
// In Ant Design 6, use role-based selector for dialog
224+
const modalAfterAdd = page.getByRole('dialog', { name: /Manage Apps/i });
213225
await expect(modalAfterAdd).toBeVisible();
214226
const numberOfApps = await modalAfterAdd.locator('.ant-form-item').count();
215227
expect(numberOfApps).toBe(numberOfAppsBeforeAdd + 1);

e2e/login.test.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import {
2-
login,
32
loginAsAdmin,
43
userInfo,
54
webServerEndpoint,
5+
webuiEndpoint,
66
} from './utils/test-util';
77
import { test, expect } from '@playwright/test';
88

99
test.beforeEach(async ({ page }) => {
10-
await page.goto('http://127.0.0.1:9081');
10+
await page.goto(webuiEndpoint);
1111
});
1212

1313
test.describe('Before login', () => {
@@ -45,9 +45,10 @@ test.describe('Login failure cases', () => {
4545
.fill(webServerEndpoint);
4646
await page.getByLabel('Login', { exact: true }).click();
4747

48+
// Wait for and verify the error notification appears
4849
await expect(
49-
page.locator('.ant-notification-notice-message'),
50-
).toContainText('Login information mismatch. Check your information');
50+
page.getByText('Login information mismatch. Check your information'),
51+
).toBeVisible();
5152
});
5253

5354
test('should display error message for incorrect password', async ({
@@ -62,8 +63,9 @@ test.describe('Login failure cases', () => {
6263
.fill(webServerEndpoint);
6364
await page.getByLabel('Login', { exact: true }).click();
6465

66+
// Wait for and verify the error notification appears
6567
await expect(
66-
page.locator('.ant-notification-notice-message'),
67-
).toContainText('Login information mismatch. Check your information');
68+
page.getByText('Login information mismatch. Check your information'),
69+
).toBeVisible();
6870
});
6971
});

e2e/maintenance.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { test, expect } from '@playwright/test';
77

88
test.beforeEach(async ({ page }) => {
99
await loginAsAdmin(page);
10-
await page.getByRole('menuitem', { name: 'Maintenance' }).click();
10+
await page.getByRole('menuitem', { name: 'Admin Settings' }).click();
11+
await page.getByRole('menuitem', { name: 'tool Maintenance' }).click();
12+
await page.waitForLoadState('networkidle');
1113
});
1214

1315
test.describe('test maintenance page', () => {

e2e/session.test.ts

Lines changed: 27 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -23,25 +23,19 @@ const createInteractiveSessionOnSessionStartPage = async (
2323
await page.waitForLoadState('networkidle');
2424

2525
// select default resource group
26-
const resourceGroup = page
27-
.locator('.ant-form-item-row:has-text("Resource Group")')
28-
.locator(
29-
'.ant-form-item-control-input-content > .ant-select > .ant-select-selector',
30-
)
31-
.locator('input');
26+
const resourceGroup = page.getByRole('combobox', {
27+
name: 'Resource Group',
28+
});
3229
await expect(resourceGroup).toBeVisible();
3330
await resourceGroup.fill('default');
34-
await page.locator('.ant-select-dropdown:has-text("default")').click();
31+
await page.keyboard.press('Enter');
3532
// select Minimum Requirements
36-
const ResourcePreset = page
37-
.locator('.ant-form-item-row:has-text("Resource Presets")')
38-
.locator(
39-
'.ant-form-item-control-input-content > .ant-select > .ant-select-selector',
40-
)
41-
.locator('input');
33+
const ResourcePreset = page.getByRole('combobox', {
34+
name: 'Resource Presets',
35+
});
4236
await expect(ResourcePreset).toBeVisible();
4337
await ResourcePreset.fill('minimum');
44-
await page.locator('.ant-select-dropdown:has-text("minimum")').click();
38+
await page.getByRole('option', { name: 'minimum' }).click();
4539
// launch
4640
await page.getByRole('button', { name: 'Skip to review' }).click();
4741

@@ -50,17 +44,11 @@ const createInteractiveSessionOnSessionStartPage = async (
5044
await expect(launchButton).toBeEnabled({ timeout: 10000 });
5145
await launchButton.click();
5246

53-
// Wait for either success or modal to appear
54-
try {
55-
await expect(page.locator('.ant-modal-confirm-title')).toHaveText(
56-
'No storage folder is mounted',
57-
{ timeout: 10000 },
58-
);
59-
await page.getByRole('button', { name: 'Start' }).click();
60-
} catch (error) {
61-
// Modal might not appear if storage is already configured
62-
console.log('No storage modal appeared, session might start directly');
63-
}
47+
await page
48+
.getByRole('dialog')
49+
.filter({ hasText: 'No storage folder is mounted' })
50+
.getByRole('button', { name: 'Start' })
51+
.click();
6452
};
6553

6654
const createBatchSessionOnSessionStartPage = async (
@@ -85,25 +73,20 @@ const createBatchSessionOnSessionStartPage = async (
8573
await page.waitForLoadState('networkidle');
8674

8775
// select default resource group
88-
const resourceGroup = page
89-
.locator('.ant-form-item-row:has-text("Resource Group")')
90-
.locator(
91-
'.ant-form-item-control-input-content > .ant-select > .ant-select-selector',
92-
)
93-
.locator('input');
76+
const resourceGroup = page.getByRole('combobox', {
77+
name: 'Resource Group',
78+
});
9479
await expect(resourceGroup).toBeVisible();
9580
await resourceGroup.fill('default');
96-
await page.locator('.ant-select-dropdown:has-text("default")').click();
81+
await page.keyboard.press('Enter');
82+
9783
// select Minimum Requirements
98-
const ResourcePreset = page
99-
.locator('.ant-form-item-row:has-text("Resource Presets")')
100-
.locator(
101-
'.ant-form-item-control-input-content > .ant-select > .ant-select-selector',
102-
)
103-
.locator('input');
84+
const ResourcePreset = page.getByRole('combobox', {
85+
name: 'Resource Presets',
86+
});
10487
await expect(ResourcePreset).toBeVisible();
10588
await ResourcePreset.fill('minimum');
106-
await page.locator('.ant-select-dropdown:has-text("minimum")').click();
89+
await page.getByRole('option', { name: 'minimum' }).click();
10790
// launch
10891
await page.getByRole('button', { name: 'Skip to review' }).click();
10992

@@ -113,16 +96,11 @@ const createBatchSessionOnSessionStartPage = async (
11396
await launchButton.click();
11497

11598
// Wait for either success or modal to appear
116-
try {
117-
await expect(page.locator('.ant-modal-confirm-title')).toHaveText(
118-
'No storage folder is mounted',
119-
{ timeout: 10000 },
120-
);
121-
await page.getByRole('button', { name: 'Start' }).click();
122-
} catch (error) {
123-
// Modal might not appear if storage is already configured
124-
console.log('No storage modal appeared, session might start directly');
125-
}
99+
await page
100+
.getByRole('dialog')
101+
.filter({ hasText: 'No storage folder is mounted' })
102+
.getByRole('button', { name: 'Start' })
103+
.click();
126104
};
127105

128106
test.describe('Session Launcher', () => {

e2e/utils/classes/FolderCreationModal.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ export class FolderCreationModal {
44
private readonly modal: Locator;
55
private readonly page: Page;
66
constructor(page: Page) {
7-
this.modal = page.locator(
8-
'.ant-modal-content:has-text("Create a new storage folder")',
9-
);
7+
this.modal = page
8+
.getByRole('dialog')
9+
.filter({ hasText: 'Create a new storage folder' });
10+
1011
this.page = page;
1112
}
1213

e2e/utils/classes/FolderExplorerModal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export class FolderExplorerModal {
55
private readonly page: Page;
66

77
constructor(page: Page) {
8-
this.modal = page.locator('.ant-modal').first();
8+
this.modal = page.getByRole('dialog').first();
99
this.page = page;
1010
}
1111

e2e/utils/classes/StartPage.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export class StartPage {
99
}
1010

1111
async goto(): Promise<void> {
12+
// If in admin settings pages, click Go Back first
13+
const goBackButton = this.page.getByRole('button', { name: 'Go Back' });
14+
if (await goBackButton.isVisible()) {
15+
await goBackButton.click();
16+
}
1217
await getMenuItem(this.page, 'Start').click();
1318
}
1419

0 commit comments

Comments
 (0)