test(user-interface-configuration): write initial tests (#1242)

* test(user-interface): add tests with playwright

* test: refactor UI configuration tests

---------

Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
This commit is contained in:
Rıdvan Akca
2023-08-25 22:31:02 +03:00
committed by GitHub
parent ddeb18f626
commit a3b3038709
20 changed files with 389 additions and 69 deletions

View File

@@ -1,7 +1,9 @@
const path = require('node:path');
const { BasePage } = require('./base-page');
const { AuthenticatedPage } = require('./authenticated-page');
export class ApplicationsPage extends AuthenticatedPage {
screenshotPath = '/applications';
export class ApplicationsPage extends BasePage {
/**
* @param {import('@playwright/test').Page} page
*/
@@ -11,12 +13,4 @@ export class ApplicationsPage extends BasePage {
this.drawerLink = this.page.getByTestId('apps-page-drawer-link');
this.addConnectionButton = this.page.getByTestId('add-connection-button');
}
async screenshot(options = {}) {
const { path: plainPath, ...restOptions } = options;
const computedPath = path.join('applications', plainPath);
return await super.screenshot({ path: computedPath, ...restOptions });
}
}

View File

@@ -0,0 +1,21 @@
const path = require('node:path');
const { expect } = require('@playwright/test');
const { BasePage } = require('./base-page');
const { LoginPage } = require('./login-page');
export class AuthenticatedPage extends BasePage {
/**
* @param {import('@playwright/test').Page} page
*/
constructor(page) {
super(page);
this.profileMenuButton = this.page.getByTestId('profile-menu-button');
this.adminMenuItem = this.page.getByRole('menuitem', { name: 'Admin' });
this.userInterfaceDrawerItem = this.page.getByTestId('user-interface-drawer-link');
this.appBar = this.page.getByTestId('app-bar');
this.goToDashboardButton = this.page.getByTestId('go-back-drawer-link');
this.typographyLogo = this.page.getByTestId('typography-logo');
this.customLogo = this.page.getByTestId('custom-logo');
}
}

View File

@@ -1,11 +1,14 @@
const path = require('node:path');
export class BasePage {
screenshotPath = '/';
/**
* @param {import('@playwright/test').Page} page
*/
constructor(page) {
this.page = page;
this.snackbar = this.page.locator('#notistack-snackbar');
}
async clickAway() {
@@ -15,7 +18,11 @@ export class BasePage {
async screenshot(options = {}) {
const { path: plainPath, ...restOptions } = options;
const computedPath = path.join('output/screenshots', plainPath);
const computedPath = path.join(
'output/screenshots',
this.screenshotPath,
plainPath
);
return await this.page.screenshot({ path: computedPath, ...restOptions });
}

View File

@@ -1,14 +1,8 @@
const path = require('node:path');
const { BasePage } = require('./base-page');
const { AuthenticatedPage } = require('./authenticated-page');
export class ConnectionsPage extends BasePage {
async screenshot(options = {}) {
const { path: plainPath, ...restOptions } = options;
const computedPath = path.join('connections', plainPath);
return await super.screenshot({ path: computedPath, ...restOptions });
}
export class ConnectionsPage extends AuthenticatedPage {
screenshotPath = '/connections';
async clickAddConnectionButton() {
await this.page.getByTestId('add-connection-button').click();

View File

@@ -1,12 +1,6 @@
const path = require('node:path');
const { BasePage } = require('./base-page');
const { AuthenticatedPage } = require('./authenticated-page');
export class ExecutionsPage extends BasePage {
async screenshot(options = {}) {
const { path: plainPath, ...restOptions } = options;
const computedPath = path.join('executions', plainPath);
return await super.screenshot({ path: computedPath, ...restOptions });
}
export class ExecutionsPage extends AuthenticatedPage {
screenshotPath = '/executions';
}

View File

@@ -1,7 +1,9 @@
const path = require('node:path');
const { BasePage } = require('./base-page');
const { AuthenticatedPage } = require('./authenticated-page');
export class FlowEditorPage extends AuthenticatedPage {
screenshotPath = '/flow-editor';
export class FlowEditorPage extends BasePage {
/**
* @param {import('@playwright/test').Page} page
*/
@@ -21,12 +23,4 @@ export class FlowEditorPage extends BasePage {
this.trigger = this.page.getByLabel('Trigger on weekends?');
this.stepCircularLoader = this.page.getByTestId('step-circular-loader');
}
async screenshot(options = {}) {
const { path: plainPath, ...restOptions } = options;
const computedPath = path.join('flow-editor', plainPath);
return await super.screenshot({ path: computedPath, ...restOptions });
}
}

View File

@@ -1,8 +1,9 @@
const { test, expect} = require('@playwright/test');
const { test, expect } = require('@playwright/test');
const { ApplicationsPage } = require('./applications-page');
const { ConnectionsPage } = require('./connections-page');
const { ExecutionsPage } = require('./executions-page');
const { FlowEditorPage } = require('./flow-editor-page');
const { UserInterfacePage } = require('./user-interface-page');
const { LoginPage } = require('./login-page');
exports.test = test.extend({
@@ -23,6 +24,9 @@ exports.test = test.extend({
flowEditorPage: async ({ page }, use) => {
await use(new FlowEditorPage(page));
},
userInterfacePage: async ({ page }, use) => {
await use(new UserInterfacePage(page));
},
});
expect.extend({
@@ -30,7 +34,7 @@ expect.extend({
await expect(locator).not.toHaveAttribute('aria-disabled', 'true');
return { pass: true };
}
},
});
exports.expect = expect;

View File

@@ -3,6 +3,8 @@ const { expect } = require('@playwright/test');
const { BasePage } = require('./base-page');
export class LoginPage extends BasePage {
path = '/login';
/**
* @param {import('@playwright/test').Page} page
*/
@@ -15,8 +17,6 @@ export class LoginPage extends BasePage {
this.loginButton = this.page.getByTestId('login-button');
}
path = '/login';
async login() {
await this.page.goto(this.path);
await this.emailTextField.fill(process.env.LOGIN_EMAIL);

View File

@@ -0,0 +1,53 @@
const path = require('node:path');
const { AuthenticatedPage } = require('./authenticated-page');
export class UserInterfacePage extends AuthenticatedPage {
screenshotPath = '/user-interface';
/**
* @param {import('@playwright/test').Page} page
*/
constructor(page) {
super(page);
this.flowRowCardActionArea = this.page
.getByTestId('flow-row')
.first()
.getByTestId('card-action-area');
this.updateButton = this.page.getByTestId('update-button');
this.primaryMainColorInput = this.page
.getByTestId('primary-main-color-input')
.getByTestId('color-text-field');
this.primaryDarkColorInput = this.page
.getByTestId('primary-dark-color-input')
.getByTestId('color-text-field');
this.primaryLightColorInput = this.page
.getByTestId('primary-light-color-input')
.getByTestId('color-text-field');
this.logoSvgCodeInput = this.page.getByTestId('logo-svg-data-text-field');
this.primaryMainColorButton = this.page
.getByTestId('primary-main-color-input')
.getByTestId('color-button');
this.primaryDarkColorButton = this.page
.getByTestId('primary-dark-color-input')
.getByTestId('color-button');
this.primaryLightColorButton = this.page
.getByTestId('primary-light-color-input')
.getByTestId('color-button');
}
hexToRgb(hexColor) {
hexColor = hexColor.replace('#', '');
const r = parseInt(hexColor.substring(0, 2), 16);
const g = parseInt(hexColor.substring(2, 4), 16);
const b = parseInt(hexColor.substring(4, 6), 16);
return `rgb(${r}, ${g}, ${b})`;
}
encodeSVG(svgCode) {
const encoded = encodeURIComponent(svgCode);
return `data:image/svg+xml;utf8,${encoded}`;
}
}

View File

@@ -0,0 +1,176 @@
// @ts-check
const { test, expect } = require('../../fixtures/index');
test.describe('User interface page', () => {
test.beforeEach(async ({ userInterfacePage }) => {
await userInterfacePage.profileMenuButton.click();
await userInterfacePage.adminMenuItem.click();
await expect(userInterfacePage.page).toHaveURL(/\/admin-settings\/users/);
await userInterfacePage.userInterfaceDrawerItem.click();
await expect(userInterfacePage.page).toHaveURL(
/\/admin-settings\/user-interface/
);
await userInterfacePage.page.waitForURL(/\/admin-settings\/user-interface/);
});
test.describe('checks if the shown values are used', async () => {
test('checks primary main color', async ({ userInterfacePage }) => {
await userInterfacePage.primaryMainColorInput.waitFor({
state: 'attached',
});
const initialPrimaryMainColor =
await userInterfacePage.primaryMainColorInput.inputValue();
const initialRgbColor = userInterfacePage.hexToRgb(
initialPrimaryMainColor
);
await expect(userInterfacePage.updateButton).toHaveCSS(
'background-color',
initialRgbColor
);
});
test('checks primary dark color', async ({ userInterfacePage }) => {
await userInterfacePage.primaryDarkColorInput.waitFor({
state: 'attached',
});
const initialPrimaryDarkColor =
await userInterfacePage.primaryDarkColorInput.inputValue();
const initialRgbColor = userInterfacePage.hexToRgb(
initialPrimaryDarkColor
);
await expect(userInterfacePage.appBar).toHaveCSS(
'background-color',
initialRgbColor
);
});
test('checks custom logo', async ({ userInterfacePage }) => {
const initialLogoSvgCode =
await userInterfacePage.logoSvgCodeInput.inputValue();
const logoSrcAttribute = await userInterfacePage.customLogo.getAttribute(
'src'
);
const svgCode = userInterfacePage.encodeSVG(initialLogoSvgCode);
expect(logoSrcAttribute).toMatch(svgCode);
});
});
test.describe(
'fill fields and check if the inputs reflect them properly',
async () => {
test('fill primary main color and check the color input', async ({
userInterfacePage,
}) => {
await userInterfacePage.primaryMainColorInput.fill('#FF5733');
const rgbColor = userInterfacePage.hexToRgb('#FF5733');
const button = await userInterfacePage.primaryMainColorButton;
const styleAttribute = await button.getAttribute('style');
expect(styleAttribute).toEqual(`background-color: ${rgbColor};`);
});
test('fill primary dark color and check the color input', async ({
userInterfacePage,
}) => {
await userInterfacePage.primaryDarkColorInput.fill('#12F63F');
const rgbColor = userInterfacePage.hexToRgb('#12F63F');
const button = await userInterfacePage.primaryDarkColorButton;
const styleAttribute = await button.getAttribute('style');
expect(styleAttribute).toEqual(`background-color: ${rgbColor};`);
});
test('fill primary light color and check the color input', async ({
userInterfacePage,
}) => {
await userInterfacePage.primaryLightColorInput.fill('#1D0BF5');
const rgbColor = userInterfacePage.hexToRgb('#1D0BF5');
const button = await userInterfacePage.primaryLightColorButton;
const styleAttribute = await button.getAttribute('style');
expect(styleAttribute).toEqual(`background-color: ${rgbColor};`);
});
}
);
test.describe(
'update form based on input values and check if the inputs still reflect them',
async () => {
test('update primary main color and check color input', async ({
userInterfacePage,
}) => {
await userInterfacePage.primaryMainColorInput.fill('#00adef');
await userInterfacePage.updateButton.click();
const rgbColor = userInterfacePage.hexToRgb('#00adef');
const button = await userInterfacePage.primaryMainColorButton;
const styleAttribute = await button.getAttribute('style');
expect(styleAttribute).toBe(`background-color: ${rgbColor};`);
});
test('update primary dark color and check color input', async ({
userInterfacePage,
}) => {
await userInterfacePage.primaryDarkColorInput.fill('#222222');
await userInterfacePage.updateButton.click();
const rgbColor = userInterfacePage.hexToRgb('#222222');
const button = await userInterfacePage.primaryDarkColorButton;
const styleAttribute = await button.getAttribute('style');
expect(styleAttribute).toBe(`background-color: ${rgbColor};`);
});
test('update primary light color and check color input', async ({
userInterfacePage,
}) => {
await userInterfacePage.primaryLightColorInput.fill('#f90707');
await userInterfacePage.updateButton.click();
const rgbColor = userInterfacePage.hexToRgb('#f90707');
const button = await userInterfacePage.primaryLightColorButton;
const styleAttribute = await button.getAttribute('style');
expect(styleAttribute).toBe(`background-color: ${rgbColor};`);
});
}
);
test.describe('update form based on input values', async () => {
test('fill primary main color', async ({ userInterfacePage }) => {
await userInterfacePage.primaryMainColorInput.fill('#00adef');
await userInterfacePage.updateButton.click();
await userInterfacePage.snackbar.waitFor({ state: 'visible' });
await userInterfacePage.screenshot({
path: 'updated primary main color.png',
});
});
test('fill primary dark color', async ({ userInterfacePage }) => {
await userInterfacePage.primaryDarkColorInput.fill('#222222');
await userInterfacePage.updateButton.click();
await userInterfacePage.snackbar.waitFor({ state: 'visible' });
await userInterfacePage.screenshot({
path: 'updated primary dark color.png',
});
});
test('fill primary light color', async ({ userInterfacePage }) => {
await userInterfacePage.primaryLightColorInput.fill('#f90707');
await userInterfacePage.updateButton.click();
await userInterfacePage.goToDashboardButton.click();
await expect(userInterfacePage.page).toHaveURL('/flows');
const span = await userInterfacePage.flowRowCardActionArea;
await span.waitFor({ state: 'visible' });
await span.hover();
await userInterfacePage.screenshot({
path: 'updated primary light color.png',
});
});
test('fill logo svg code', async ({ userInterfacePage }) => {
await userInterfacePage.logoSvgCodeInput
.fill(`<svg width="25" height="25" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 100 100">
<rect width="100%" height="100%" fill="white" />
<text x="10" y="40" font-family="Arial" font-size="40" fill="black">A</text>
</svg>`);
await userInterfacePage.updateButton.click();
await userInterfacePage.snackbar.waitFor({ state: 'visible' });
await userInterfacePage.screenshot({
path: 'updated svg code.png',
});
});
});
});

View File

@@ -42,6 +42,7 @@ function createDrawerLinks({
Icon: GroupIcon,
primary: 'adminSettingsDrawer.users',
to: URLS.USERS,
dataTest: 'users-drawer-link',
}
: null,
canReadRole
@@ -49,6 +50,7 @@ function createDrawerLinks({
Icon: GroupsIcon,
primary: 'adminSettingsDrawer.roles',
to: URLS.ROLES,
dataTest: 'roles-drawer-link',
}
: null,
canUpdateConfig
@@ -56,6 +58,7 @@ function createDrawerLinks({
Icon: BrushIcon,
primary: 'adminSettingsDrawer.userInterface',
to: URLS.USER_INTERFACE,
dataTest: 'user-interface-drawer-link',
}
: null,
canManageSamlAuthProvider
@@ -63,6 +66,7 @@ function createDrawerLinks({
Icon: LockIcon,
primary: 'adminSettingsDrawer.authentication',
to: URLS.AUTHENTICATION,
dataTest: 'authentication-drawer-link',
}
: null,
].filter(Boolean) as DrawerLink[];
@@ -75,6 +79,7 @@ const drawerBottomLinks = [
Icon: ArrowBackIosNewIcon,
primary: 'adminSettingsDrawer.goBack',
to: '/',
dataTest: 'go-back-drawer-link',
},
];

View File

@@ -46,7 +46,7 @@ export default function AppBar(props: AppBarProps): React.ReactElement {
};
return (
<MuiAppBar>
<MuiAppBar data-test="app-bar">
<Container maxWidth={maxWidth} disableGutters>
<Toolbar>
<IconButton

View File

@@ -0,0 +1,40 @@
import React from 'react';
import { ButtonProps } from '@mui/material/Button';
import { Button } from './style';
const BG_IMAGE_FALLBACK =
'linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(135deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(135deg, transparent 75%, #ccc 75%) /*! @noflip */';
export type ColorButtonProps = Omit<ButtonProps, 'children'> & {
bgColor: string;
isBgColorValid: boolean;
disablePopover: boolean;
};
export type ColorButtonElement = (props: ColorButtonProps) => JSX.Element;
const ColorButton = (props: ColorButtonProps) => {
const {
bgColor,
className,
disablePopover,
isBgColorValid,
...restButtonProps
} = props;
return (
<Button
data-test="color-button"
disableTouchRipple
style={{
backgroundColor: isBgColorValid ? bgColor : undefined,
backgroundImage: isBgColorValid ? undefined : BG_IMAGE_FALLBACK,
cursor: disablePopover ? 'default' : undefined,
}}
className={`MuiColorInput-Button ${className || ''}`}
{...restButtonProps}
/>
);
};
export default ColorButton;

View File

@@ -0,0 +1,15 @@
import MuiButton from '@mui/material/Button';
import { styled } from '@mui/material/styles';
export const Button = styled(MuiButton)(() => ({
backgroundSize: '8px 8px',
backgroundPosition: '0 0, 4px 0, 4px -4px, 0px 4px',
transition: 'none',
boxShadow: '0 4px 6px rgba(50, 50, 93, 0.11), 0 1px 3px rgba(0, 0, 0, 0.08)',
border: 0,
borderRadius: 4,
width: '24px',
aspectRatio: '1 / 1',
height: '24px',
minWidth: 0,
})) as typeof MuiButton;

View File

@@ -1,6 +1,7 @@
import * as React from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { MuiColorInput, MuiColorInputProps } from 'mui-color-input';
import ColorButton from './ColorButton';
type ColorInputProps = {
shouldUnregister?: boolean;
@@ -15,7 +16,6 @@ export default function ColorInput(props: ColorInputProps): React.ReactElement {
name,
shouldUnregister = false,
disabled = false,
'data-test': dataTest,
...textFieldProps
} = props;
@@ -27,12 +27,13 @@ export default function ColorInput(props: ColorInputProps): React.ReactElement {
shouldUnregister={shouldUnregister}
render={({ field }) => (
<MuiColorInput
Adornment={ColorButton}
format="hex"
{...textFieldProps}
{...field}
disabled={disabled}
inputProps={{
'data-test': dataTest,
'data-test': 'color-text-field',
}}
/>
)}

View File

@@ -8,7 +8,10 @@ const CustomLogo = () => {
const logoSvgData = config['logo.svgData'] as string;
return (
<img src={`data:image/svg+xml;utf8,${encodeURIComponent(logoSvgData)}`} />
<img
data-test="custom-logo"
src={`data:image/svg+xml;utf8,${encodeURIComponent(logoSvgData)}`}
/>
);
};

View File

@@ -68,7 +68,8 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
</div>
<List sx={{ py: 0, mt: 3 }}>
{bottomLinks.map(({ Icon, badgeContent, primary, to }, index) => (
{bottomLinks.map(
({ Icon, badgeContent, primary, to, dataTest }, index) => (
<ListItemLink
key={`${to}-${index}`}
icon={
@@ -79,8 +80,10 @@ export default function Drawer(props: DrawerProps): React.ReactElement {
primary={formatMessage(primary)}
to={to}
onClick={closeOnClick}
data-test={dataTest}
/>
))}
)
)}
</List>
</BaseDrawer>
);

View File

@@ -18,7 +18,7 @@ type FlowRowProps = {
flow: IFlow;
};
function getFlowStatusTranslationKey(status: IFlow["status"]): string {
function getFlowStatusTranslationKey(status: IFlow['status']): string {
if (status === 'published') {
return 'flow.published';
} else if (status === 'paused') {
@@ -28,7 +28,16 @@ function getFlowStatusTranslationKey(status: IFlow["status"]): string {
return 'flow.draft';
}
function getFlowStatusColor(status: IFlow["status"]): 'default' | 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' {
function getFlowStatusColor(
status: IFlow['status']
):
| 'default'
| 'primary'
| 'secondary'
| 'error'
| 'info'
| 'success'
| 'warning' {
if (status === 'published') {
return 'success';
} else if (status === 'paused') {
@@ -64,8 +73,12 @@ export default function FlowRow(props: FlowRowProps): React.ReactElement {
return (
<>
<Card sx={{ mb: 1 }}>
<CardActionArea component={Link} to={URLS.FLOW(flow.id)}>
<Card sx={{ mb: 1 }} data-test="flow-row">
<CardActionArea
component={Link}
to={URLS.FLOW(flow.id)}
data-test="card-action-area"
>
<CardContent>
<Apps direction="row" gap={1} sx={{ gridArea: 'apps' }}>
<FlowAppIcons steps={flow.steps} />
@@ -98,9 +111,7 @@ export default function FlowRow(props: FlowRowProps): React.ReactElement {
size="small"
color={getFlowStatusColor(flow?.status)}
variant={flow?.active ? 'filled' : 'outlined'}
label={formatMessage(
getFlowStatusTranslationKey(flow?.status)
)}
label={formatMessage(getFlowStatusTranslationKey(flow?.status))}
/>
<IconButton

View File

@@ -9,12 +9,12 @@ const Logo = () => {
const { config, loading } = useConfig(['logo.svgData']);
const logoSvgData = config?.['logo.svgData'] as string;
if (loading && !logoSvgData) return (<React.Fragment />);
if (loading && !logoSvgData) return <React.Fragment />;
if (logoSvgData) return <CustomLogo />;
return (
<Typography variant="h6" component="h1" noWrap>
<Typography variant="h6" component="h1" data-test="typography-logo" noWrap>
<FormattedMessage id="brandText" />
</Typography>
);

View File

@@ -90,18 +90,21 @@ export default function UserInterface(): React.ReactElement {
name="palette.primary.main"
label={formatMessage('userInterfacePage.mainColor')}
fullWidth
data-test="primary-main-color-input"
/>
<ColorInput
name="palette.primary.dark"
label={formatMessage('userInterfacePage.darkColor')}
fullWidth
data-test="primary-dark-color-input"
/>
<ColorInput
name="palette.primary.light"
label={formatMessage('userInterfacePage.lightColor')}
fullWidth
data-test="primary-light-color-input"
/>
<TextField
@@ -109,6 +112,7 @@ export default function UserInterface(): React.ReactElement {
label={formatMessage('userInterfacePage.svgData')}
multiline
fullWidth
data-test="logo-svg-data-text-field"
/>
<LoadingButton
@@ -117,6 +121,7 @@ export default function UserInterface(): React.ReactElement {
color="primary"
sx={{ boxShadow: 2 }}
loading={loading}
data-test="update-button"
>
{formatMessage('userInterfacePage.submit')}
</LoadingButton>