tests/playwright: Add Hostname customization test
Adds a Hostname customization test that creates a BP, edits BP, exports and imports it back and verifies the BP content. This test also servers as a template for other customization tests. Includes a refactor of existing helper functions.
This commit is contained in:
parent
91ceaca760
commit
0bddb80e94
10 changed files with 341 additions and 49 deletions
|
|
@ -60,3 +60,6 @@ overrides:
|
|||
extends: "plugin:testing-library/react"
|
||||
- files: ["playwright/**/*.ts"]
|
||||
extends: "plugin:playwright/recommended"
|
||||
rules:
|
||||
playwright/no-conditional-in-test: off
|
||||
playwright/no-conditional-expect: off
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export default defineConfig({
|
|||
? process.env.BASE_URL
|
||||
: 'http://127.0.0.1:9090',
|
||||
video: 'retain-on-failure',
|
||||
trace: 'on-first-retry',
|
||||
trace: 'on',
|
||||
|
||||
ignoreHTTPSErrors: true,
|
||||
},
|
||||
|
|
|
|||
84
playwright/Customizations/Hostname.spec.ts
Normal file
84
playwright/Customizations/Hostname.spec.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { test } from '../fixtures/cleanup';
|
||||
import { isHosted } from '../helpers/helpers';
|
||||
import { login } from '../helpers/login';
|
||||
import { navigateToOptionalSteps, ibFrame } from '../helpers/navHelpers';
|
||||
import {
|
||||
registerLater,
|
||||
fillInDetails,
|
||||
createBlueprint,
|
||||
fillInImageOutputGuest,
|
||||
deleteBlueprint,
|
||||
exportBlueprint,
|
||||
importBlueprint,
|
||||
} from '../helpers/wizardHelpers';
|
||||
|
||||
test('Create a blueprint with Hostname customization', async ({
|
||||
page,
|
||||
cleanup,
|
||||
}) => {
|
||||
const blueprintName = 'test-' + uuidv4();
|
||||
const hostname = 'testsystem';
|
||||
|
||||
// Delete the blueprint after the run fixture
|
||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||
|
||||
// Login, navigate to IB and get the frame
|
||||
await login(page);
|
||||
const frame = await ibFrame(page);
|
||||
|
||||
await test.step('Navigate to optional steps in Wizard', async () => {
|
||||
await navigateToOptionalSteps(frame);
|
||||
await registerLater(frame);
|
||||
});
|
||||
|
||||
await test.step('Select and fill the Hostname step', async () => {
|
||||
await frame.getByRole('button', { name: 'Hostname' }).click();
|
||||
await frame.getByRole('textbox', { name: 'hostname input' }).fill(hostname);
|
||||
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
||||
});
|
||||
|
||||
await test.step('Fill the BP details', async () => {
|
||||
await fillInDetails(frame, blueprintName);
|
||||
});
|
||||
|
||||
await test.step('Create BP', async () => {
|
||||
await createBlueprint(frame, blueprintName);
|
||||
});
|
||||
|
||||
await test.step('Edit BP', async () => {
|
||||
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
||||
await frame.getByLabel('Revisit Hostname step').click();
|
||||
await frame.getByRole('textbox', { name: 'hostname input' }).click();
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'hostname input' })
|
||||
.fill(hostname + 'edited');
|
||||
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
||||
await frame
|
||||
.getByRole('button', { name: 'Save changes to blueprint' })
|
||||
.click();
|
||||
});
|
||||
|
||||
// This is for hosted service only as these features are not available in cockpit plugin
|
||||
await test.step('Export BP', async (step) => {
|
||||
step.skip(!isHosted(), 'Exporting is not available in the plugin');
|
||||
await exportBlueprint(page, blueprintName);
|
||||
});
|
||||
|
||||
await test.step('Import BP', async (step) => {
|
||||
step.skip(!isHosted(), 'Importing is not available in the plugin');
|
||||
await importBlueprint(page, blueprintName);
|
||||
});
|
||||
|
||||
await test.step('Review imported BP', async (step) => {
|
||||
step.skip(!isHosted(), 'Importing is not available in the plugin');
|
||||
await fillInImageOutputGuest(page);
|
||||
await page.getByRole('button', { name: 'Hostname' }).click();
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'hostname input' })
|
||||
).toHaveValue(hostname + 'edited');
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
});
|
||||
});
|
||||
45
playwright/fixtures/cleanup.ts
Normal file
45
playwright/fixtures/cleanup.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { test as oldTest } from '@playwright/test';
|
||||
|
||||
type WithCleanup = {
|
||||
cleanup: Cleanup;
|
||||
};
|
||||
|
||||
export interface Cleanup {
|
||||
add: (cleanupFn: () => Promise<unknown>) => symbol;
|
||||
runAndAdd: (cleanupFn: () => Promise<unknown>) => Promise<symbol>;
|
||||
remove: (key: symbol) => void;
|
||||
}
|
||||
|
||||
export const test = oldTest.extend<WithCleanup>({
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
cleanup: async ({}, use) => {
|
||||
const cleanupFns: Map<symbol, () => Promise<unknown>> = new Map();
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
await use({
|
||||
add: (cleanupFn) => {
|
||||
const key = Symbol();
|
||||
cleanupFns.set(key, cleanupFn);
|
||||
return key;
|
||||
},
|
||||
runAndAdd: async (cleanupFn) => {
|
||||
await cleanupFn();
|
||||
|
||||
const key = Symbol();
|
||||
cleanupFns.set(key, cleanupFn);
|
||||
return key;
|
||||
},
|
||||
remove: (key) => {
|
||||
cleanupFns.delete(key);
|
||||
},
|
||||
});
|
||||
|
||||
await test.step(
|
||||
'Post-test cleanup',
|
||||
async () => {
|
||||
await Promise.all(Array.from(cleanupFns).map(([, fn]) => fn()));
|
||||
},
|
||||
{ box: true }
|
||||
);
|
||||
},
|
||||
});
|
||||
35
playwright/helpers/helpers.ts
Normal file
35
playwright/helpers/helpers.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { type Page, expect } from '@playwright/test';
|
||||
|
||||
export const togglePreview = async (page: Page) => {
|
||||
const toggleSwitch = page.locator('#preview-toggle');
|
||||
|
||||
if (!(await toggleSwitch.isChecked())) {
|
||||
await toggleSwitch.click();
|
||||
}
|
||||
|
||||
const turnOnButton = page.getByRole('button', { name: 'Turn on' });
|
||||
if (await turnOnButton.isVisible()) {
|
||||
await turnOnButton.click();
|
||||
}
|
||||
|
||||
await expect(toggleSwitch).toBeChecked();
|
||||
};
|
||||
|
||||
export const isHosted = (): boolean => {
|
||||
return process.env.BASE_URL?.includes('redhat.com') || false;
|
||||
};
|
||||
|
||||
export const closePopupsIfExist = async (page: Page) => {
|
||||
const locatorsToCheck = [
|
||||
page.locator('.pf-v5-c-alert.notification-item button'), // This closes all toast pop-ups
|
||||
page.locator(`button[id^="pendo-close-guide-"]`), // This closes the pendo guide pop-up
|
||||
page.locator(`button[id="truste-consent-button"]`), // This closes the trusted consent pop-up
|
||||
page.getByLabel('close-notification'), // This closes a one off info notification (May be covered by the toast above, needs recheck.)
|
||||
];
|
||||
|
||||
for (const locator of locatorsToCheck) {
|
||||
await page.addLocatorHandler(locator, async () => {
|
||||
await locator.first().click(); // There can be multiple toast pop-ups
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -1,29 +1,11 @@
|
|||
import { type Page, type FrameLocator, expect } from '@playwright/test';
|
||||
import { type Page, expect } from '@playwright/test';
|
||||
|
||||
export const ibFrame = (page: Page): FrameLocator | Page => {
|
||||
if (isHosted()) {
|
||||
return page;
|
||||
}
|
||||
return page
|
||||
.locator('iframe[name="cockpit1\\:localhost\\/cockpit-image-builder"]')
|
||||
.contentFrame();
|
||||
};
|
||||
|
||||
export const togglePreview = async (page: Page) => {
|
||||
const toggleSwitch = page.locator('#preview-toggle');
|
||||
|
||||
if (!(await toggleSwitch.isChecked())) {
|
||||
await toggleSwitch.click();
|
||||
}
|
||||
|
||||
const turnOnButton = page.getByRole('button', { name: 'Turn on' });
|
||||
if (await turnOnButton.isVisible()) {
|
||||
await turnOnButton.click();
|
||||
}
|
||||
|
||||
await expect(toggleSwitch).toBeChecked();
|
||||
};
|
||||
import { closePopupsIfExist, isHosted, togglePreview } from './helpers';
|
||||
|
||||
/**
|
||||
* Logs in to either Cockpit or Console, will distinguish between them based on the environment
|
||||
* @param page - the page object
|
||||
*/
|
||||
export const login = async (page: Page) => {
|
||||
if (!process.env.PLAYWRIGHT_USER || !process.env.PLAYWRIGHT_PASSWORD) {
|
||||
throw new Error('user or password not set in environment');
|
||||
|
|
@ -38,10 +20,6 @@ export const login = async (page: Page) => {
|
|||
return loginCockpit(page, user, password);
|
||||
};
|
||||
|
||||
export const isHosted = (): boolean => {
|
||||
return process.env.BASE_URL?.includes('redhat.com') || false;
|
||||
};
|
||||
|
||||
const loginCockpit = async (page: Page, user: string, password: string) => {
|
||||
await page.goto('/cockpit-image-builder');
|
||||
|
||||
|
|
@ -68,10 +46,13 @@ const loginCockpit = async (page: Page, user: string, password: string) => {
|
|||
}
|
||||
|
||||
// expect to have administrative access
|
||||
await page.getByRole('button', { name: 'Administrative access' });
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Administrative access' })
|
||||
).toBeVisible();
|
||||
};
|
||||
|
||||
const loginConsole = async (page: Page, user: string, password: string) => {
|
||||
await closePopupsIfExist(page);
|
||||
await page.goto('/insights/image-builder/landing');
|
||||
await page
|
||||
.getByRole('textbox', { name: 'Red Hat login or email' })
|
||||
|
|
@ -79,22 +60,6 @@ const loginConsole = async (page: Page, user: string, password: string) => {
|
|||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
await page.getByRole('textbox', { name: 'Password' }).fill(password);
|
||||
await page.getByRole('button', { name: 'Log in' }).click();
|
||||
await closePopupsIfExist(page);
|
||||
await togglePreview(page);
|
||||
await page.getByRole('heading', { name: 'All images' });
|
||||
};
|
||||
|
||||
const closePopupsIfExist = async (page: Page) => {
|
||||
const locatorsToCheck = [
|
||||
page.locator('.pf-v5-c-alert.notification-item button'), // This closes all toast pop-ups
|
||||
page.locator(`button[id^="pendo-close-guide-"]`), // This closes the pendo guide pop-up
|
||||
page.locator(`button[id="truste-consent-button"]`), // This closes the trusted consent pop-up
|
||||
page.getByLabel('close-notification'), // This closes a one off info notification (May be covered by the toast above, needs recheck.)
|
||||
];
|
||||
|
||||
for (const locator of locatorsToCheck) {
|
||||
await page.addLocatorHandler(locator, async () => {
|
||||
await locator.first().click(); // There can be multiple toast pop-ups
|
||||
});
|
||||
}
|
||||
await expect(page.getByRole('heading', { name: 'All images' })).toBeVisible();
|
||||
};
|
||||
26
playwright/helpers/navHelpers.ts
Normal file
26
playwright/helpers/navHelpers.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import type { FrameLocator, Page } from '@playwright/test';
|
||||
|
||||
import { isHosted } from './helpers';
|
||||
|
||||
/**
|
||||
* Opens the wizard, fills out the "Image Output" step, and navigates to the optional steps
|
||||
* @param page - the page object
|
||||
*/
|
||||
export const navigateToOptionalSteps = async (page: Page | FrameLocator) => {
|
||||
await page.getByRole('button', { name: 'Create blueprint' }).click();
|
||||
await page.getByRole('checkbox', { name: 'Virtualization' }).click();
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the FrameLocator object in case we are using cockpit plugin, else it returns the page object
|
||||
* @param page - the page object
|
||||
*/
|
||||
export const ibFrame = (page: Page): FrameLocator | Page => {
|
||||
if (isHosted()) {
|
||||
return page;
|
||||
}
|
||||
return page
|
||||
.locator('iframe[name="cockpit1\\:localhost\\/cockpit-image-builder"]')
|
||||
.contentFrame();
|
||||
};
|
||||
129
playwright/helpers/wizardHelpers.ts
Normal file
129
playwright/helpers/wizardHelpers.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { expect, FrameLocator, type Page, test } from '@playwright/test';
|
||||
|
||||
import { isHosted } from './helpers';
|
||||
import { ibFrame } from './navHelpers';
|
||||
|
||||
/**
|
||||
* Clicks the create button, handles the modal, clicks the button again and selecets the BP in the list
|
||||
* @param page - the page object
|
||||
* @param blueprintName - the name of the created blueprint
|
||||
*/
|
||||
export const createBlueprint = async (
|
||||
page: Page | FrameLocator,
|
||||
blueprintName: string
|
||||
) => {
|
||||
await page.getByRole('button', { name: 'Create blueprint' }).click();
|
||||
await page.getByRole('button', { name: 'Close' }).first().click();
|
||||
await page.getByRole('button', { name: 'Create blueprint' }).click();
|
||||
await page.getByRole('textbox', { name: 'Search input' }).fill(blueprintName);
|
||||
await page.getByTestId('blueprint-card').getByText(blueprintName).click();
|
||||
};
|
||||
|
||||
/**
|
||||
* Fill in the "Details" step in the wizard
|
||||
* This method assumes that the "Details" step is ENABLED!
|
||||
* After filling the step, it will click the "Next" button
|
||||
* Description defaults to "Testing blueprint"
|
||||
* @param page - the page object
|
||||
* @param blueprintName - the name of the blueprint to create
|
||||
*/
|
||||
export const fillInDetails = async (
|
||||
page: Page | FrameLocator,
|
||||
blueprintName: string
|
||||
) => {
|
||||
await page.getByRole('listitem').filter({ hasText: 'Details' }).click();
|
||||
await page
|
||||
.getByRole('textbox', { name: 'Blueprint name' })
|
||||
.fill(blueprintName);
|
||||
await page
|
||||
.getByRole('textbox', { name: 'Blueprint description' })
|
||||
.fill('Testing blueprint');
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
};
|
||||
|
||||
/**
|
||||
* Select "Register later" option in the wizard
|
||||
* This function executes only on the hosted service
|
||||
* @param page - the page object
|
||||
*/
|
||||
export const registerLater = async (page: Page | FrameLocator) => {
|
||||
if (isHosted()) {
|
||||
await page.getByRole('button', { name: 'Register' }).click();
|
||||
await page.getByRole('radio', { name: 'Register later' }).click();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fill in the image output step in the wizard by selecting the Guest Image
|
||||
* @param page - the page object
|
||||
*/
|
||||
export const fillInImageOutputGuest = async (page: Page | FrameLocator) => {
|
||||
await page.getByRole('checkbox', { name: 'Virtualization' }).click();
|
||||
await page.getByRole('button', { name: 'Next' }).click();
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete the blueprint with the given name
|
||||
* Will locate to the Image Builder page and search for the blueprint first
|
||||
* @param page - the page object
|
||||
* @param blueprintName - the name of the blueprint to delete
|
||||
*/
|
||||
export const deleteBlueprint = async (page: Page, blueprintName: string) => {
|
||||
await test.step(
|
||||
'Delete the blueprint with name: ' + blueprintName,
|
||||
async () => {
|
||||
// Locate back to the Image Builder page every time because the test can fail at any stage
|
||||
const frame = await ibFrame(page);
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'Search input' })
|
||||
.fill(blueprintName);
|
||||
await frame
|
||||
.getByTestId('blueprint-card')
|
||||
.getByText(blueprintName)
|
||||
.click();
|
||||
await frame.getByRole('button', { name: 'Menu toggle' }).click();
|
||||
await frame.getByRole('menuitem', { name: 'Delete blueprint' }).click();
|
||||
await frame.getByRole('button', { name: 'Delete' }).click();
|
||||
},
|
||||
{ box: true }
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Export the blueprint
|
||||
* This function executes only on the hosted service
|
||||
* @param page - the page object
|
||||
*/
|
||||
export const exportBlueprint = async (page: Page, blueprintName: string) => {
|
||||
if (isHosted()) {
|
||||
await page.getByRole('button', { name: 'Menu toggle' }).click();
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
await page
|
||||
.getByRole('menuitem', { name: 'Download blueprint (.json)' })
|
||||
.click();
|
||||
const download = await downloadPromise;
|
||||
await download.saveAs('../../downloads/' + blueprintName + '.json');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Import the blueprint
|
||||
* This function executes only on the hosted service
|
||||
* @param page - the page object
|
||||
*/
|
||||
export const importBlueprint = async (
|
||||
page: Page | FrameLocator,
|
||||
blueprintName: string
|
||||
) => {
|
||||
if (isHosted()) {
|
||||
await page.getByRole('button', { name: 'Import' }).click();
|
||||
const dragBoxSelector = page.getByRole('presentation').first();
|
||||
await dragBoxSelector
|
||||
.locator('input[type=file]')
|
||||
.setInputFiles('../../downloads/' + blueprintName + '.json');
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'File upload' })
|
||||
).not.toBeEmpty();
|
||||
await page.getByRole('button', { name: 'Review and Finish' }).click();
|
||||
}
|
||||
};
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
import { expect, test } from '@playwright/test';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { login, ibFrame, isHosted } from './lib/lib';
|
||||
import { isHosted } from './helpers/helpers';
|
||||
import { login } from './helpers/login';
|
||||
import { ibFrame } from './helpers/navHelpers';
|
||||
|
||||
test.describe.serial('test', () => {
|
||||
const blueprintName = uuidv4();
|
||||
|
|
@ -76,7 +78,9 @@ test.describe.serial('test', () => {
|
|||
await frame.getByTestId('close-button-saveandbuild-modal').click();
|
||||
await frame.getByRole('button', { name: 'Create blueprint' }).click();
|
||||
|
||||
await frame.getByText(blueprintName);
|
||||
await expect(
|
||||
frame.locator('.pf-v5-c-card__title-text').getByText(blueprintName)
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('edit blueprint', async ({ page }) => {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ const BlueprintCard = ({ blueprint }: blueprintProps) => {
|
|||
<Card
|
||||
isSelected={blueprint.id === selectedBlueprintId}
|
||||
ouiaId={`blueprint-card-${blueprint.id}`}
|
||||
data-testid={`blueprint-card`}
|
||||
isCompact
|
||||
isClickable
|
||||
onClick={() => dispatch(setBlueprintId(blueprint.id))}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue