Skip to content

Commit 16d0686

Browse files
test(rbac): add playwright tests for the plugin (#1305)
1 parent 0512028 commit 16d0686

File tree

4 files changed

+289
-1
lines changed

4 files changed

+289
-1
lines changed

plugins/rbac/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
"prepack": "backstage-cli package prepack",
2323
"start": "backstage-cli package start",
2424
"test": "backstage-cli package test --passWithNoTests --coverage",
25-
"tsc": "tsc"
25+
"tsc": "tsc",
26+
"ui-test": "yarn playwright test"
2627
},
2728
"dependencies": {
2829
"@backstage/catalog-model": "^1.4.4",
@@ -54,6 +55,7 @@
5455
"@backstage/dev-utils": "1.0.27",
5556
"@backstage/test-utils": "1.5.0",
5657
"@janus-idp/cli": "1.7.5",
58+
"@playwright/test": "1.41.2",
5759
"@testing-library/jest-dom": "6.4.2",
5860
"@testing-library/react": "14.2.1",
5961
"@testing-library/react-hooks": "8.0.1",

plugins/rbac/playwright.config.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { defineConfig, devices } from '@playwright/test';
2+
3+
/**
4+
* See https://playwright.dev/docs/test-configuration.
5+
*/
6+
export default defineConfig({
7+
testDir: './tests',
8+
/* Run tests in files in parallel */
9+
fullyParallel: true,
10+
/* Fail the build on CI if you accidentally left test.only in the source code. */
11+
forbidOnly: !!process.env.CI,
12+
/* Retry on CI only */
13+
retries: process.env.CI ? 2 : 0,
14+
/* Run tests in sequence. */
15+
workers: 1,
16+
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
17+
reporter: 'html',
18+
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
19+
use: {
20+
baseURL: process.env.PLUGIN_BASE_URL || 'http://localhost:3000',
21+
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
22+
trace: 'on-first-retry',
23+
screenshot: 'only-on-failure',
24+
video: 'retain-on-failure',
25+
},
26+
27+
/* Configure projects for major browsers */
28+
projects: [
29+
{
30+
name: 'chromium',
31+
use: { ...devices['Desktop Chrome'] },
32+
},
33+
],
34+
});

plugins/rbac/tests/rbac.spec.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { expect, Page, test } from '@playwright/test';
2+
3+
import {
4+
clickButton,
5+
verifyCellsInTable,
6+
verifyColumnHeading,
7+
verifyText,
8+
} from './rbacHelper';
9+
10+
test.describe('RBAC plugin', () => {
11+
let page: Page;
12+
13+
test.beforeAll(async ({ browser }) => {
14+
const context = await browser.newContext();
15+
page = await context.newPage();
16+
await page.goto('/');
17+
const navSelector = 'nav [aria-label="Administration"]';
18+
await page.locator(navSelector).click();
19+
await expect(
20+
page.getByRole('heading', { name: 'Administration' }),
21+
).toBeVisible({ timeout: 20000 });
22+
const rbacTabSelector = page.getByRole('tab', { name: 'RBAC' });
23+
await expect(rbacTabSelector).toBeVisible({
24+
timeout: 20000,
25+
});
26+
});
27+
28+
test.afterAll(async ({ browser }) => {
29+
await browser.close();
30+
});
31+
32+
test('Should show 2 roles in the list, column headings and cells', async () => {
33+
await expect(
34+
page.getByRole('heading', { name: 'All roles (2)' }),
35+
).toBeVisible({ timeout: 20000 });
36+
37+
const columns = [
38+
'Name',
39+
'Users and groups',
40+
'Permission Policies',
41+
'Actions',
42+
];
43+
await verifyColumnHeading(columns, page);
44+
45+
const roleName = new RegExp(/^(role|user|group):[a-zA-Z]+\/[\w@*.~-]+$/);
46+
const usersAndGroups = new RegExp(
47+
/^(1\s(user|group)|[2-9]\s(users|groups))(, (1\s(user|group)|[2-9]\s(users|groups)))?$/,
48+
);
49+
const permissionPolicies = /\d/;
50+
const cellIdentifier = [roleName, usersAndGroups, permissionPolicies];
51+
52+
await verifyCellsInTable(cellIdentifier, page);
53+
});
54+
55+
test('View details of role', async () => {
56+
const roleName = 'role:default/rbac_admin';
57+
await page.locator(`a`).filter({ hasText: roleName }).click();
58+
await expect(page.getByRole('heading', { name: roleName })).toBeVisible({
59+
timeout: 20000,
60+
});
61+
62+
await expect(page.getByRole('tab', { name: 'Overview' })).toBeVisible({
63+
timeout: 20000,
64+
});
65+
await expect(page.getByText('About')).toBeVisible();
66+
67+
// verify users and groups table
68+
await expect(
69+
page.getByRole('heading', { name: 'Users and groups (1 user, 1 group)' }),
70+
).toBeVisible({ timeout: 20000 });
71+
72+
await verifyColumnHeading(['Name', 'Type', 'Members'], page);
73+
74+
const name = new RegExp(/^(\w+)$/);
75+
const type = new RegExp(/^(User|Group)$/);
76+
const members = /^(-|\d+)$/;
77+
const userGroupCellIdentifier = [name, type, members];
78+
await verifyCellsInTable(userGroupCellIdentifier, page);
79+
80+
// verify permission policy table
81+
await expect(
82+
page.getByRole('heading', { name: 'Permission policies (5)' }),
83+
).toBeVisible({ timeout: 20000 });
84+
await verifyColumnHeading(['Plugin', 'Permission', 'Policies'], page);
85+
const policies =
86+
/^(?:(Read|Create|Update|Delete)(?:, (?:Read|Create|Update|Delete))*|Use)$/;
87+
await verifyCellsInTable([policies], page);
88+
89+
await page.locator(`a`).filter({ hasText: 'RBAC' }).click();
90+
});
91+
92+
test('Edit user from overview page', async () => {
93+
const roleName = 'role:default/rbac_admin';
94+
await page.locator(`a`).filter({ hasText: roleName }).click();
95+
await expect(page.getByRole('heading', { name: roleName })).toBeVisible({
96+
timeout: 20000,
97+
});
98+
await page.getByRole('tab', { name: 'Overview' }).click();
99+
100+
const RoleOverviewPO = {
101+
updatePolicies: 'span[data-testid="update-policies"]',
102+
updateMembers: 'span[data-testid="update-members"]',
103+
};
104+
105+
await page.locator(RoleOverviewPO.updateMembers).click();
106+
await expect(page.getByRole('heading', { name: 'Edit Role' })).toBeVisible({
107+
timeout: 20000,
108+
});
109+
await page
110+
.getByPlaceholder('Search by user name or group name')
111+
.fill('Guest Use');
112+
await page.getByText('Guest User').click();
113+
await expect(
114+
page.getByRole('heading', {
115+
name: 'Users and groups (2 users, 1 group)',
116+
}),
117+
).toBeVisible({
118+
timeout: 20000,
119+
});
120+
await clickButton('Next', page);
121+
await clickButton('Next', page);
122+
await clickButton('Save', page);
123+
await verifyText('Role role:default/rbac_admin updated successfully', page);
124+
125+
// edit/update policies
126+
await page.locator(RoleOverviewPO.updatePolicies).click();
127+
await expect(page.getByRole('heading', { name: 'Edit Role' })).toBeVisible({
128+
timeout: 20000,
129+
});
130+
131+
await page.getByTestId('AddIcon').click();
132+
await page.getByPlaceholder('Select a plugin').last().click();
133+
await page.getByText('scaffolder').click();
134+
await page.getByPlaceholder('Select a permission').last().click();
135+
await page.getByText('scaffolder-action').click();
136+
137+
await clickButton('Next', page);
138+
await clickButton('Save', page);
139+
await verifyText('Role role:default/rbac_admin updated successfully', page);
140+
141+
await page.locator(`a`).filter({ hasText: 'RBAC' }).click();
142+
});
143+
144+
test('Create role from rolelist page', async () => {
145+
await expect(
146+
page.getByRole('heading', { name: 'All roles (2)' }),
147+
).toBeVisible({ timeout: 20000 });
148+
149+
// create-role
150+
await page.getByTestId('create-role').click();
151+
await expect(
152+
page.getByRole('heading', { name: 'Create role' }),
153+
).toBeVisible({ timeout: 20000 });
154+
155+
await page.fill('input[name="name"]', 'sample-role-1');
156+
await page.fill('textarea[name="description"]', 'Test Description data');
157+
await clickButton('Next', page);
158+
159+
await page
160+
.getByPlaceholder('Search by user name or group name')
161+
.fill('Guest Use');
162+
await page.getByText('Guest User').click();
163+
await expect(
164+
page.getByRole('heading', {
165+
name: 'Users and groups (1 user)',
166+
}),
167+
).toBeVisible({
168+
timeout: 20000,
169+
});
170+
await clickButton('Next', page);
171+
172+
await page.getByPlaceholder('Select a plugin').first().click();
173+
await page.getByText('scaffolder').click();
174+
await page.getByPlaceholder('Select a permission').last().click();
175+
await page.getByText('scaffolder-action').click();
176+
await clickButton('Next', page);
177+
178+
await clickButton('Create', page);
179+
await verifyText(
180+
'Role role:default/sample-role-1 created successfully',
181+
page,
182+
);
183+
});
184+
});

plugins/rbac/tests/rbacHelper.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { expect, Page } from '@playwright/test';
2+
3+
export const verifyCellsInTable = async (
4+
cellIdentifier: (string | RegExp)[],
5+
page: Page,
6+
) => {
7+
for (const text of cellIdentifier) {
8+
const cellLocator = page
9+
.locator('td[class*="MuiTableCell-root"]')
10+
.filter({ hasText: text });
11+
const count = await cellLocator.count();
12+
13+
if (count === 0) {
14+
throw new Error(
15+
`Expected at least one cell with text matching ${text}, but none were found.`,
16+
);
17+
}
18+
19+
// Checks if all matching cells are visible.
20+
for (let i = 0; i < count; i++) {
21+
await expect(cellLocator.nth(i)).toBeVisible();
22+
}
23+
}
24+
};
25+
26+
export const verifyColumnHeading = async (
27+
columns: (string | RegExp)[],
28+
page: Page,
29+
) => {
30+
const thead = page.locator('thead');
31+
for (const col of columns) {
32+
await expect(
33+
thead.getByRole('columnheader', { name: col, exact: true }),
34+
).toBeVisible();
35+
}
36+
};
37+
38+
export const clickButton = async (
39+
label: string,
40+
page: Page,
41+
options: { exact?: boolean; force?: boolean } = {
42+
exact: true,
43+
force: false,
44+
},
45+
) => {
46+
const selector = `span[class^="MuiButton-label"]:has-text("${label}")`;
47+
const button = page
48+
.locator(selector)
49+
.getByText(label, { exact: options.exact })
50+
.first();
51+
await button.waitFor({ state: 'visible' });
52+
53+
if (options?.force) {
54+
await button.click({ force: true });
55+
} else {
56+
await button.click();
57+
}
58+
};
59+
60+
export const verifyText = async (
61+
text: string | RegExp,
62+
page: Page,
63+
exact: boolean = true,
64+
) => {
65+
const element = page.getByText(text, { exact: exact }).first();
66+
await element.scrollIntoViewIfNeeded();
67+
await expect(element).toBeVisible();
68+
};

0 commit comments

Comments
 (0)