-
Notifications
You must be signed in to change notification settings - Fork 622
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
9a12e51
commit 2e47a2c
Showing
6 changed files
with
435 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
66 changes: 66 additions & 0 deletions
66
frontend/integration-tests/tests/metalkube/dashboard.scenario.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
36
frontend/integration-tests/views/metalkube/dashboards.view.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
180 changes: 180 additions & 0 deletions
180
frontend/public/components/dashboard/inventory-card/inventory-item.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.