Skip to content

Commit

Permalink
Testing infra for metalkubed
Browse files Browse the repository at this point in the history
This is the basic infrastructure for the baremetal dashboards.
Currently the test reads some of the counters in the dashboards page,
and compares them to what we get from the CLI.

This PR includes PRs 2287 and 2288 from Dan Trainor, who added custom
data-test-id attributes on the elements in the dashboards, to support
the automation so it will be easier to find the elements.

Basic steps to run the tests:

yarn install
yarn webdriver-update
export BRIDGE_BASE_ADDRESS='https://*.*.*.redhat.com'
export BRIDGE_AUTH_USERNAME=kubeadmin
export BRIDGE_AUTH_PASSWORD=22Z....
oc login -s *.*.*.redhat.com:6443 -u kubeadmin -p 22ZW....
export KUBECONFIG=/home/ukalifon/.kube/config
export NO_HEADLESS=true    # optional - if you want to see the browser
yarn run test-suite --suite baremetalSmokeTests --params.openshift true
  • Loading branch information
ukalifon authored and knowncitizen committed Oct 23, 2019
1 parent 9a12e51 commit 2e47a2c
Show file tree
Hide file tree
Showing 6 changed files with 435 additions and 5 deletions.
23 changes: 18 additions & 5 deletions frontend/integration-tests/protractor.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,24 @@ export const config: Config = {
return new Promise((resolve) => htmlReporter.afterLaunch(resolve.bind(this, exitCode)));
},
suites: {
filter: suite(['tests/filter.scenario.ts']),
annotation: suite(['tests/modal-annotations.scenario.ts']),
environment: suite(['tests/environment.scenario.ts']),
secrets: suite(['tests/secrets.scenario.ts']),
storage: suite(['tests/storage.scenario.ts']),
baremetalSmokeTests: [
'tests/metalkube/dashboard.scenario.ts',
],
filter: suite([
'tests/filter.scenario.ts',
]),
annotation: suite([
'tests/modal-annotations.scenario.ts',
]),
environment: suite([
'tests/environment.scenario.ts',
]),
secrets: suite([
'tests/secrets.scenario.ts',
]),
storage: suite([
'tests/storage.scenario.ts',
]),
crud: suite([
'tests/crud.scenario.ts',
'tests/secrets.scenario.ts',
Expand Down
66 changes: 66 additions & 0 deletions frontend/integration-tests/tests/metalkube/dashboard.scenario.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { browser, ExpectedConditions as until } from 'protractor';
const execSync = require('child_process').execSync;

import { appHost } from '../../protractor.conf';
import * as dashboardView from '../../views/metalkube/dashboards.view';

async function compareCounter(elem, expectedValue) {
const displayedValue = Number(await dashboardView.getTextIfPresent(elem, '0'));
expect(displayedValue).toEqual(expectedValue);
}

describe('Inventory card', () => {
beforeAll(async() => {
await browser.get(`${appHost}/dashboards`);
await dashboardView.isLoaded();
// wait until the counters in the inventory card show up
await browser.wait(
until.or(
until.presenceOf(dashboardView.inventoryNodesUpCounter),
until.presenceOf(dashboardView.inventoryNodesDownCounter)));
await browser.wait(
until.or(
until.presenceOf(dashboardView.inventoryHostsUpCounter),
until.presenceOf(dashboardView.inventoryHostsDownCounter)));
});

it('Node count is displayed', async() => {
// get the number of ready and not ready nodes from the CLI
let readyNodes = 0;
const output = execSync('kubectl get nodes -o json', { encoding: 'utf-8' });
const nodes = JSON.parse(output);
nodes.items.forEach((node) => {
node.status.conditions.forEach((condition) => {
if (condition.reason === 'KubeletReady' && condition.status === 'True') {
readyNodes++;
}
});
});
const displayedLabel = await dashboardView.inventoryNodesItemLabel.getText();
// comparing if the dashboards are displaying ${nodes.items.length} nodes total
expect(displayedLabel).toEqual(`${nodes.items.length} Nodes`);
// comparing if the dashboards are displaying ${readyNodes} ready nodes
compareCounter(dashboardView.inventoryNodesUpCounter, readyNodes);
// comparing if the dashboards are displaying ${nodes.items.length - readyNodes} not-ready nodes
compareCounter(dashboardView.inventoryNodesDownCounter, nodes.items.length - readyNodes);
});

it('Host count is displayed', async() => {
// get the hosts and their statuses from the CLI
let readyHosts = 0;
const output = execSync('kubectl get baremetalhosts -n openshift-machine-api -o json', { encoding: 'utf-8' });
const hosts = JSON.parse(output);
hosts.items.forEach((host) => {
if (host.status.operationalStatus === 'OK') {
readyHosts++;
}
});
const displayedLabel = await dashboardView.inventoryHostsItemLabel.getText();
// comparing if the dashboards are displaying ${hosts.items.length} hosts total
expect(displayedLabel).toEqual(`${hosts.items.length} Bare Metal Hosts`);
// comparing if the dashboards are displaying ${readyHosts} ready hosts
compareCounter(dashboardView.inventoryHostsUpCounter, readyHosts);
// comparing if the dashboards are displaying ${hosts.items.length - readyHosts} not-ready hosts
compareCounter(dashboardView.inventoryHostsDownCounter, hosts.items.length - readyHosts);
});
});
36 changes: 36 additions & 0 deletions frontend/integration-tests/views/metalkube/dashboards.view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { browser, $, $$ } from 'protractor';
import { waitForNone } from '../../protractor.conf';

export const untilNoLoadersPresent = waitForNone($$('.co-m-loader'));
export const isLoaded = () => browser.wait(untilNoLoadersPresent).then(() => browser.sleep(2000));

export const inventoryNodesItemLabel = $('[data-test-id="console-dashboard-inventory-node"]')
.$('.co-inventory-card__item-title');
export const inventoryNodesUpCounter = $('[data-test-id="console-dashboard-inventory-node"]')
.$('[data-test-id="console-dashboard-inventory-count-ready"]');
export const inventoryNodesDownCounter = $('[data-test-id="console-dashboard-inventory-node"]')
.$('[data-test-id="console-dashboard-inventory-count-notready"]');

export const inventoryPodsItemLabel = $('[data-test-id="console-dashboard-inventory-pod"]')
.$('.co-inventory-card__item-title');
export const inventoryPodsUpCounter = $('[data-test-id="console-dashboard-inventory-pod"]')
.$('[data-test-id="console-dashboard-inventory-count-running-succeeded"]');
export const inventoryPodsDownCounter = $('[data-test-id="console-dashboard-inventory-pod"]')
.$('[data-test-id="console-dashboard-inventory-count-crashloopbackoff-failed"]');

export const inventoryHostsItemLabel = $('[data-test-id="console-dashboard-inventory-baremetalhost"]')
.$('.co-inventory-card__item-title');
export const inventoryHostsUpCounter = $('[data-test-id="console-dashboard-inventory-baremetalhost"]')
.$('[data-test-id="console-dashboard-inventory-count-ready-provisioned"]');
export const inventoryHostsDownCounter = $('[data-test-id="console-dashboard-inventory-baremetalhost"]')
.$('[data-test-id="console-dashboard-inventory-count-notready"]');

// Utility function: getTextIfPresent
export async function getTextIfPresent(elem, textIfNotPresent='') {
if (await elem.isPresent()) {
return elem.getText();
}
return new Promise(resolve => {
resolve(textIfNotPresent);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ const InventoryCard: React.FC<InventoryCardProps> = ({
title="Pod"
count={podCount}
error={podQueryError || podError || !podStats.length}
data-test-id="metal3-plugin-dashboard-inventory-pod"
/>
</InventoryBody>
</DashboardCardBody>
Expand Down
180 changes: 180 additions & 0 deletions frontend/public/components/dashboard/inventory-card/inventory-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import { InProgressIcon, QuestionCircleIcon } from '@patternfly/react-icons';

import {
RedExclamationCircleIcon,
YellowExclamationTriangleIcon,
} from '@console/shared';
import * as plugins from '../../../plugins';
import {
LoadingInline,
} from '../../utils';
import {
K8sResourceKind,
K8sKind
} from '../../../module/k8s';
import {
InventoryStatusGroup
} from '@console/shared/src/components/dashboard/inventory-card/status-group';
import {
connectToFlags,
FlagsObject,
WithFlagsProps
} from '../../../reducers/features';
import {
getFlagsForExtensions,
isDashboardExtensionInUse,
} from '@console/internal/components/dashboard/utils';

const defaultStatusGroupIcons = {
[InventoryStatusGroup.WARN]: (
<YellowExclamationTriangleIcon />
),
[InventoryStatusGroup.ERROR]: (
<RedExclamationCircleIcon />
),
[InventoryStatusGroup.PROGRESS]: (
<InProgressIcon className="co-inventory-card__status-icon--progress" />
),
[InventoryStatusGroup.NOT_MAPPED]: (
<QuestionCircleIcon className="co-inventory-card__status-icon--question" />
),
};

const getStatusGroupIcons = (flags: FlagsObject) => {
const groupStatusIcons = {...defaultStatusGroupIcons};
plugins.registry.getDashboardsInventoryItemGroups().filter(e => isDashboardExtensionInUse(e, flags)).forEach(group => {
if (!groupStatusIcons[group.properties.id]) {
groupStatusIcons[group.properties.id] = group.properties.icon;
}
});
return groupStatusIcons;
};

export const InventoryItem: React.FC<InventoryItemProps> = React.memo(
({ isLoading, singularTitle, pluralTitle, count, children, error = false, ...props }) => {
const title = count !== 1 ? pluralTitle : singularTitle;
let status: React.ReactNode;
if (error) {
status = <div className="co-dashboard-text--small text-secondary">Unavailable</div>;
} else if (isLoading) {
status = <LoadingInline />;
} else {
status = children;
}
return (
<div data-test-id={props['data-test-id']} className="co-inventory-card__item">
<div className="co-inventory-card__item-title">{isLoading || error ? title : `${count} ${title}`}</div>
<div className="co-inventory-card__item-status">{status}</div>
</div>
);
}
);

export const Status: React.FC<StatusProps> = React.memo(({ groupID, count, flags }) => {
const statusGroupIcons = getStatusGroupIcons(flags);
const groupIcon = statusGroupIcons[groupID] || statusGroupIcons[InventoryStatusGroup.NOT_MAPPED];
return (
<div className="co-inventory-card__status">
<span className="co-dashboard-icon">{groupIcon}</span>
<span className="co-inventory-card__status-text">{count}</span>
</div>
);
});

const StatusLink: React.FC<StatusLinkProps> = React.memo(
({ groupID, count, statusIDs, kind, namespace, filterType, flags }) => {
const statusItems = encodeURIComponent(statusIDs.join(','));
const namespacePath = namespace ? `ns/${namespace}` : 'all-namespaces';
const cleanStatusItems = statusIDs.join('-').toLowerCase();
const to = filterType && statusItems.length > 0 ? `/k8s/${namespacePath}/${kind.plural}?rowFilter-${filterType}=${statusItems}` : `/k8s/${namespacePath}/${kind.plural}`;
const statusGroupIcons = getStatusGroupIcons(flags);
const groupIcon = statusGroupIcons[groupID] || statusGroupIcons[InventoryStatusGroup.NOT_MAPPED];
return (
<div className="co-inventory-card__status">
<Link to={to} style={{textDecoration: 'none'}}>
<span className="co-dashboard-icon">{groupIcon}</span>
<span data-test-id={`console-dashboard-inventory-count-${ cleanStatusItems }`} className="co-inventory-card__status-text">{count}</span>
</Link>
</div>
);
}
);

export const ResourceInventoryItem = connectToFlags<ResourceInventoryItemProps>(
...getFlagsForExtensions(plugins.registry.getDashboardsInventoryItemGroups()),
)(React.memo(
({ kind, useAbbr, resources, additionalResources, isLoading, mapper, namespace, error, showLink = true, flags = {}, ...props }) => {
const groups = mapper(resources, additionalResources);
const [singularTitle, pluralTitle] = useAbbr ? [kind.abbr, `${kind.abbr}s`] : [kind.label, kind.labelPlural];
return (
<InventoryItem
isLoading={isLoading}
singularTitle={singularTitle}
pluralTitle={pluralTitle}
count={resources.length}
error={error}
data-test-id={props['data-test-id']}
>
{Object.keys(groups).filter(key => groups[key].count > 0).map(key => showLink ?
(
<StatusLink
key={key}
kind={kind}
namespace={namespace}
groupID={key}
count={groups[key].count}
statusIDs={groups[key].statusIDs}
filterType={groups[key].filterType}
flags={flags}
/>
) : (
<Status
groupID={key}
count={groups[key].count}
flags={flags}
/>
)
)}
</InventoryItem>
);
}
));

export type StatusGroupMapper = (resources: K8sResourceKind[], additionalResources?: {[key: string]: K8sResourceKind[]}) => {[key in InventoryStatusGroup | string]: {filterType?: string, statusIDs: string[], count: number}};

type InventoryItemProps = {
isLoading: boolean;
singularTitle: string;
pluralTitle: string;
count: number;
children?: React.ReactNode;
error: boolean;
'data-test-id'?: string;
};

type StatusProps = WithFlagsProps & {
groupID: InventoryStatusGroup | string;
count: number;
}

type StatusLinkProps = StatusProps & {
statusIDs: string[];
kind: K8sKind;
namespace?: string;
filterType?: string;
}

type ResourceInventoryItemProps = WithFlagsProps & {
resources: K8sResourceKind[];
additionalResources?: {[key: string]: K8sResourceKind[]};
mapper: StatusGroupMapper;
kind: K8sKind;
useAbbr?: boolean;
isLoading: boolean;
namespace?: string;
error: boolean;
showLink?: boolean;
'data-test-id'?: string;
}
Loading

0 comments on commit 2e47a2c

Please sign in to comment.