Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions integration-tests/cypress.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { defineConfig } = require('cypress');

module.exports = defineConfig({
chromeWebSecurity: false,
defaultCommandTimeout: 30000,
e2e: {
setupNodeEvents(on, config) {
Expand All @@ -10,6 +11,7 @@ module.exports = defineConfig({
supportFile: 'support/index.ts',
},
fixturesFolder: 'fixtures',
pageLoadTimeout: 120000,
reporter: '../../node_modules/cypress-multi-reporters',
reporterOptions: {
configFile: 'reporter-config.json',
Expand Down
6 changes: 6 additions & 0 deletions integration-tests/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ module.exports = (on, config) => {
},
};
on('file:preprocessor', wp(options));
on('before:browser:launch', (browser = {}, launchOptions) => {
if (browser.family === 'chromium' || browser.name === 'electron') {
launchOptions.args.push('--ignore-certificate-errors');
}
return launchOptions;
});
// `config` is the resolved Cypress config
config.baseUrl = `${process.env.BRIDGE_BASE_ADDRESS || 'http://localhost:9000/'}`;
config.env.BRIDGE_KUBEADMIN_PASSWORD = process.env.BRIDGE_KUBEADMIN_PASSWORD;
Expand Down
1 change: 1 addition & 0 deletions integration-tests/support/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Import commands.js using ES2015 syntax:
import './login';
import './selectors';

export const checkErrors = () =>
cy.window().then((win) => {
Expand Down
73 changes: 57 additions & 16 deletions integration-tests/support/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,78 @@ declare global {
}

const KUBEADMIN_USERNAME = 'kubeadmin';
const loginUsername = Cypress.env('BRIDGE_KUBEADMIN_PASSWORD') ? 'user-dropdown' : 'username';
const loggedInSelector =
'[data-test="user-dropdown-toggle"], [data-test="user-dropdown"], [data-test="username"]';
const kubeadminAliases = new Set(['kubeadmin', 'kube:admin']);

type ConsoleWindow = { SERVER_FLAGS?: { authDisabled?: boolean } } & Window;

const typeLoginForm = (user: string, pwd: string) => {
cy.get('#inputUsername').type(user);
cy.get('#inputPassword').type(pwd, { log: false });
cy.get('#co-login-button, button[type=submit]').click();
};

const displayedIdentity = ($body: JQuery<HTMLElement>): string =>
$body.find(loggedInSelector).first().text().trim().toLowerCase();

const isDisplayedUser = (user: string, $body: JQuery<HTMLElement>): boolean => {
const displayed = displayedIdentity($body);
const requested = user.toLowerCase();
if (kubeadminAliases.has(requested)) {
return kubeadminAliases.has(displayed) || displayed.includes('kube:admin');
}
return Boolean(displayed) && displayed.includes(requested);
};

const logoutFromMasthead = () => {
cy.get(loggedInSelector).first().click();
cy.get('[data-test="log-out"]').should('be.visible');
cy.get('[data-test="log-out"]').click({ force: true });
};

// This will add 'cy.login(...)'
// ex: cy.login('my-user', 'my-password')
Cypress.Commands.add('login', (username: string, password: string) => {
// Check if auth is disabled (for a local development environment).
cy.visit('/'); // visits baseUrl which is set in plugins/index.js
cy.window().then((win) => {
Cypress.Commands.add('login', (username?: string, password?: string) => {
const user = username || KUBEADMIN_USERNAME;
const pwd = password || Cypress.env('BRIDGE_KUBEADMIN_PASSWORD');

cy.visit('/');
cy.window().then((win: ConsoleWindow) => {
if (win.SERVER_FLAGS?.authDisabled) {
return;
}

// Make sure we clear the cookie in case a previous test failed to logout.
cy.clearCookie('openshift-session-token');
cy.get(`#inputUsername, ${loggedInSelector}`, { timeout: 60000 }).should('exist');
cy.get('body').then(($body) => {
if ($body.find('#inputUsername').length) {
typeLoginForm(user, pwd);
return;
}

cy.get('#inputUsername').type(username || KUBEADMIN_USERNAME);
cy.get('#inputPassword').type(password || Cypress.env('BRIDGE_KUBEADMIN_PASSWORD'));
cy.get('button[type=submit]').click();
const reuseSession = !username || isDisplayedUser(user, $body);
if (reuseSession) {
return;
}

cy.get(`[data-test="${loginUsername}"]`).should('be.visible');
logoutFromMasthead();
cy.get('#inputUsername', { timeout: 60000 }).should('be.visible');
typeLoginForm(user, pwd);
});
cy.get(loggedInSelector, { timeout: 120000 }).should('be.visible');
});
});

Cypress.Commands.add('logout', () => {
// Check if auth is disabled (for a local development environment).
cy.window().then((win) => {
cy.window().then((win: ConsoleWindow) => {
if (win.SERVER_FLAGS?.authDisabled) {
return;
}
cy.get('[data-test="user-dropdown"]').click();
cy.get('[data-test="log-out"]').should('be.visible');
cy.get('[data-test="log-out"]').click({ force: true });
cy.get('body').then(($body) => {
if (!$body.find(loggedInSelector).length) {
return;
}
logoutFromMasthead();
});
});
});
26 changes: 26 additions & 0 deletions integration-tests/support/selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
declare global {
namespace Cypress {
interface Chainable {
byLegacyTestID(
selector: string,
options?: Partial<
Cypress.Loggable & Cypress.Shadow & Cypress.Timeoutable & Cypress.Withinable
>,
): Chainable<JQuery<HTMLElement>>;
byTestID(
selector: string,
options?: Partial<
Cypress.Loggable & Cypress.Shadow & Cypress.Timeoutable & Cypress.Withinable
>,
): Chainable<JQuery<HTMLElement>>;
}
}
}

Cypress.Commands.add('byTestID', (selector, options) => {
cy.get(`[data-test="${selector}"]`, options);
});

Cypress.Commands.add('byLegacyTestID', (selector, options) => {
cy.get(`[data-test-id="${selector}"]`, options);
});
222 changes: 222 additions & 0 deletions integration-tests/support/service-form.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
export const SERVICE_FORM_NS = 'default';

const byTestOr = (dataTest: string, fallback: string): Cypress.Chainable<JQuery<HTMLElement>> =>
cy.get(`[data-test="${dataTest}"], ${fallback}`);

export const uniqueServiceName = (prefix: string): string =>
`${prefix}-${Date.now().toString().slice(-8)}`;

export const serviceFormUrl = (namespace = SERVICE_FORM_NS): string =>
`/k8s/ns/${namespace}/core~v1~Service/~new/form`;

export const serviceDetailsUrl = (name: string, namespace = SERVICE_FORM_NS): string =>
`/k8s/ns/${namespace}/core~v1~Service/${name}`;

export const serviceEditUrl = (name: string, namespace = SERVICE_FORM_NS): string =>
`/k8s/ns/${namespace}/core~v1~Service/${name}/form`;

export const dismissGuidedTourIfPresent = (): void => {
cy.get('body').then(($body) => {
if ($body.find('[data-test="tour-step-footer-secondary"]').length) {
cy.byTestID('tour-step-footer-secondary').click();
}
});
};

export const serviceNameField = () => byTestOr('service-name', '#service-name');
export const serviceNamespaceField = () => byTestOr('service-namespace', '#service-namespace');
export const serviceTypeToggle = () => byTestOr('service-type', '#toggle-service-type');
export const servicePortsField = () => byTestOr('service-ports', '#service-ports');
export const serviceExternalNameField = () =>
byTestOr('service-external-name', '#service-external-name');
export const saveChangesButton = () => byTestOr('save-changes', '#save-changes');
export const selectorKeyField = () =>
byTestOr('pairs-list-name', 'input[aria-labelledby="editor-label-header"]');
export const selectorValueField = () =>
byTestOr('pairs-list-value', 'input[aria-labelledby="editor-selector-header"]');
export const addSelectorButton = () => byTestOr('pairs-list-add', 'button:contains("Add label")');
export const deleteSelectorButton = () =>
byTestOr('pairs-list-delete', '[data-test-id="pairs-list__delete-from-btn"]');
export const actionsToggle = () => byTestOr('service-actions-toggle', 'button:contains("Actions")');

export const visitServiceCreateForm = (namespace = SERVICE_FORM_NS): void => {
cy.visit(serviceFormUrl(namespace));
dismissGuidedTourIfPresent();
cy.contains('h2', 'Create Service', { timeout: 90000 }).should('be.visible');
cy.contains('label', 'Form view', { timeout: 60000 }).click();
serviceNameField().should('be.visible');
};

export const visitServiceEditForm = (name: string, namespace = SERVICE_FORM_NS): void => {
cy.visit(serviceEditUrl(name, namespace));
dismissGuidedTourIfPresent();
cy.contains('h2', 'Edit Service', { timeout: 90000 }).should('be.visible');
cy.contains('label', 'Form view', { timeout: 60000 }).click();
serviceNameField().should('be.visible');
};

export const selectServiceType = (
type: 'ClusterIP' | 'ExternalName' | 'LoadBalancer' | 'NodePort',
) => {
serviceTypeToggle().click();
cy.get(`[data-test="service-type-${type}"], [role="menuitem"]`).contains(type).click();
serviceTypeToggle().should('contain', type);
};

const setNativeInputValue = (input: HTMLInputElement | HTMLTextAreaElement, value: string) => {
const win = input.ownerDocument.defaultView;
if (!win) {
return;
}
const proto =
input.tagName === 'TEXTAREA'
? win.HTMLTextAreaElement.prototype
: win.HTMLInputElement.prototype;
const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
setter?.call(input, value);
input.dispatchEvent(new win.Event('input', { bubbles: true }));
input.dispatchEvent(new win.Event('change', { bubbles: true }));
};

export const fillServiceName = (name: string): void => {
serviceNameField().then(($el) => {
setNativeInputValue($el[0] as HTMLInputElement, name);
});
serviceNameField().should('have.value', name);
};

export const fillSelector = (key: string, value: string, index = 0): void => {
selectorKeyField()
.eq(index)
.then(($el) => {
setNativeInputValue($el[0] as HTMLInputElement, key);
});
selectorValueField()
.eq(index)
.then(($el) => {
setNativeInputValue($el[0] as HTMLInputElement, value);
});
};

export const fillPorts = (portsText: string): void => {
servicePortsField().then(($el) => {
setNativeInputValue($el[0] as HTMLTextAreaElement, portsText);
});
servicePortsField().should('have.value', portsText);
};

export const submitServiceForm = (): void => {
saveChangesButton().should('not.be.disabled').click();
};

export const getYamlEditorValue = (): Cypress.Chainable<string> =>
cy.window().then((win) => {
const monaco = (
win as {
monaco?: { editor: { getModels: () => { getValue: () => string }[] } };
} & Window
).monaco;
const fromMonaco = monaco?.editor
?.getModels()
?.map((model) => model.getValue())
.find((value) => value?.trim());
if (fromMonaco) {
return fromMonaco;
}
const lines = win.document.querySelector('.yaml-editor .view-lines');
return (lines?.textContent || '').replace(/\u00a0/g, ' ');
});

export const setYamlEditorValue = (value: string): void => {
cy.window().then((win) => {
const monaco = (
win as {
monaco?: {
editor: {
getModels: () => { getValue: () => string; setValue: (next: string) => void }[];
};
};
} & Window
).monaco;
const models = monaco?.editor?.getModels() ?? [];
const target = models.find((model) => model.getValue()?.trim()) || models[0];
if (!target) {
throw new Error('YAML editor is not available: no Monaco model found');
}
target.setValue(value);
});
};

export const switchToYamlView = (): void => {
cy.contains('label', 'YAML view').click();
cy.get('.yaml-editor', { timeout: 30000 }).should('be.visible');
cy.get('.yaml-editor .view-line', { timeout: 30000 }).should('contain', 'kind');
};

export const switchToFormView = (): void => {
cy.contains('label', 'Form view').click();
serviceNameField().should('be.visible');
};

export const getService = (name: string, namespace = SERVICE_FORM_NS) =>
cy.request({
failOnStatusCode: false,
url: `/api/kubernetes/api/v1/namespaces/${namespace}/services/${name}`,
});

export const expectServiceSpec = (
name: string,
expected: { externalName?: string; selector?: Record<string, string>; type: string },
namespace = SERVICE_FORM_NS,
) => {
getService(name, namespace).then((response) => {
expect(response.status, `Service ${name} should exist`).to.eq(200);
expect(response.body?.spec?.type).to.eq(expected.type);
if (expected.externalName) {
expect(response.body?.spec?.externalName).to.eq(expected.externalName);
}
if (expected.selector) {
expect(response.body?.spec?.selector).to.include(expected.selector);
}
});
};

export const expectYamlToContain = (...snippets: string[]): void => {
cy.get('.yaml-editor', { timeout: 30000 }).should('be.visible');
cy.get('.yaml-editor .view-lines', { timeout: 30000 })
.invoke('text')
.then((text) => {
const normalized = String(text).replace(/\u00a0/g, ' ');
snippets.forEach((snippet) => {
expect(normalized, `YAML should contain "${snippet}"`).to.include(snippet);
});
});
};

export const confirmDeleteModal = (name: string): void => {
cy.get('body').then(($body) => {
const nameInput = $body.find(
'[data-test="delete-resource-modal"], input#resource-name, [data-test="confirm-modal-resource"]',
);
if (nameInput.length) {
cy.wrap(nameInput.first()).clear();
cy.wrap(nameInput.first()).type(name);
}
});
cy.byTestID('confirm-action').click();
};

export const deleteServiceFromDetails = (name: string, namespace = SERVICE_FORM_NS): void => {
cy.location('pathname').then((pathname) => {
if (!String(pathname).includes(`/${name}`)) {
cy.visit(serviceDetailsUrl(name, namespace));
}
});
dismissGuidedTourIfPresent();
cy.contains('h1', name, { timeout: 60000 }).should('be.visible');
actionsToggle().should('be.visible').click();
cy.get('[data-test="delete-services"], [data-test-id="delete-services"]').click();
confirmDeleteModal(name);
cy.location('pathname', { timeout: 30000 }).should('match', /service/i);
cy.location('pathname').should('not.include', `/${name}`);
};
Loading