Cypress E2E Automation Framework (TypeScript, POM, CI-ready)
Cypress Automation Portfolio
Cypress TypeScript framework with a domain-driven Page Object Model, reusable helpers, and
CI-ready E2E suites, visualized with a modern multi-color architecture diagram.
Architecture Diagram
Page Objects
(UI / POM)
Test Suites
(E2E flows)
Utilities / Helpers
(Requests, data)
Purple represents UI Page Objects, blue covers orchestrating E2E test suites, yellow holds shared
utilities and HTTP helpers. The arrows show the direction of dependency: tests rely on page objects
and utilities, never the opposite.
Core Code Snippets
loginPage.ts
import
import
import
import
import
mockLogin from 'fixtures/shared/mockLogin';
CommonRequests from '../Common/CommonRequests';
AccountType from '../../../src/types/Components/AccountType';
{UserData} from '../../../src/types/UserData';
{CountryEnum} from '@tallarna/unit-conversion';
export default class LoginPage {
public static interceptCaptchaValidation() {
cy.intercept('POST', '/accounts/verifyCaptcha', {success: true}).as(
'captchaValidation'
);
}
public static interceptLoginSuccess() {
cy.intercept('POST', '/accounts/login', mockLogin()).as('login');
}
public static interceptUserData(userData?: Partial) {
cy.intercept('GET', 'accounts/userByCognitoId', {
id: userData?.id ?? 'user-id',
account_id: userData?.accountId ?? 'account-id',
account_country: userData?.accountCountry ?? CountryEnum.GB,
first_name: userData?.firstName ?? 'Testing',
last_name: userData?.lastName ?? 'User',
permissions: userData?.permissions ?? [AccountType.SUPER_ADMIN],
user_email: userData?.email ??-,
}).as('getUserData');
}
static loginAndVisit(path: string, userData?: Partial) {
LoginPage.bypassLogin(userData);
cy.visitURL(path);
}
public static bypassLogin(
userData: Partial | undefined = undefined,
expiration?: number
) {
CommonRequests.intercept();
LoginPage.interceptUserData(userData);
window.localStorage.setItem(
'authorizationObject',
JSON.stringify(mockLogin(expiration))
);
}
static bypassLoginNoIntercept() {
window.localStorage.setItem(
'authorizationObject',
JSON.stringify(mockLogin())
);
}
public static login(userData: Partial | undefined = undefined) {
CommonRequests.intercept();
LoginPage.interceptUserData(userData);
LoginPage.interceptLoginSuccess();
LoginPage.interceptCaptchaValidation();
cy.visitURL('/').then(window => {
if (window.localStorage.getItem('authorizationObject') === null) {-, {log: false});
cy.get('[name="password"]').type('password', {log: false});
cy.get('[data-test="login-form-submit-button"]').click();
}
});
}
}
DigitalTwinEditor.ts
const buildingId = '123';
const childrenBuildingId = '456';
const fixturePath = 'Buildings/Building/DigitalTwinEditor';
export const getFixture = (file: string) => ({
fixture: `${fixturePath}/${file}.json`,
});
export enum SYSTEMS {
OBJECT = 'OBJECT',
COLLECTION = 'COLLECTION',
}
const COMPONENTS = {
[SYSTEMS.OBJECT]: {
visibleFields: 'VISIBLE_FIELDS',
withHidden: 'WITH_HIDDEN',
},
[SYSTEMS.COLLECTION]: {
1: {component: 'COL1'},
2: {component: 'COL1'},
},
};
export enum FIXTURES {
getSystems = 'getSystems',
getBuilding = 'getBuilding',
getChildrenBuilding = 'getChildrenBuilding',
getSimulation = 'getSimulation',
getChildrenBuildings = 'getChildrenBuildings',
getObject = 'getObject',
getCollection = 'getCollection',
createCollection = 'createCollection',
deleteCollection = 'deleteCollection',
getV2 = 'getV2',
getDigitalTwinMetadata = 'getDigitalTwinMetadata',
}
const interceptDigitalTwinRequests = (children?: string) => {
// Systems are used to render tabs in the page
cy.intercept(
'POST',
'/meta-data/model-systems/*',
getFixture(FIXTURES.getSystems)
).as(FIXTURES.getSystems);
// Request for the V2
cy.intercept(
'GET',
'/flow-manager/digital-twin/*',
getFixture(FIXTURES.getV2)
).as(FIXTURES.getV2);
// Building data are used to validate its existence and render related components
const buildingFixture = children
? FIXTURES.getChildrenBuilding
: FIXTURES.getBuilding;
cy.intercept(
'GET',
`/buildings/${children ?? buildingId}`,
getFixture(buildingFixture)
).as(buildingFixture);
// Building children data are used to render switch to children buildings if they exists
cy.intercept(
'POST',
`/buildings/${buildingId}/children/*`,
getFixture(FIXTURES.getChildrenBuildings)
).as(FIXTURES.getChildrenBuildings);
// Object Components are used to render tabs in the page
cy.intercept(
'GET',
'/digital-twin/java/*/OBJECT',
getFixture(FIXTURES.getObject)
).as(FIXTURES.getObject);
// Collection Components are used to render tabs in the page
cy.intercept(
'GET',
CommonRequests.ts
const interceptDictionaries = () => {
// Intercept dictionary strings request
cy.intercept('GET', '/apex-web-UK.json', {
fixture: 'shared/apex-web-UK.json',
}).as('getDictionary');
// Intercept dictionary strings request
cy.intercept('GET', '/apex-web-US.json', {
fixture: 'shared/apex-web-US.json',
}).as('getDictionary');
};
const interceptBuildingsMetadata = () => {
cy.intercept('GET', '/buildings/metadata', {
fixture: 'Metadata/buildingsMetadata.json',
}).as('buildingsMetadata');
};
const interceptG2N = () => {
cy
.intercept('GET', '/g2n/report', {
fixture: 'GrossToNet/Gross/report.json',
})
.as('getReports'),
cy
.intercept('GET', '/g2n/report/1234ab56c78def-/gross', {
fixture: 'GrossToNet/Gross/gross.json',
})
.as('getGross'),
cy
.intercept('GET', '/g2n/report/1234ab56c78def-/external', {
fixture: 'GrossToNet/Gross/external.json',
})
.as('getExternal'),
cy
.intercept('GET', '/g2n/report/1234ab56c78def-/net', {
fixture: 'GrossToNet/Gross/net.json',
})
.as('getNet');
};
const interceptOpportunitiesMetadata = () => {
cy.intercept('GET', '/opportunities/metadata', {
fixture: 'Metadata/opportunitiesMetadata.json',
}).as('opportunitiesMetadata');
};
const interceptMetadata = () => {
interceptBuildingsMetadata();
interceptOpportunitiesMetadata();
};
const setConsentCookie = () => {
cy.setCookie('userConsent', JSON.stringify({analytics: false}));
};
export default {
intercept: function () {
// Intercept all portfolios request (used in global filter)
cy.intercept('GET', '/portfolios/account?page=1&pageSize=9999', {
fixture: 'Settings/Portfolios/new-portfolios.json',
}).as('getPortfolios');
interceptMetadata();
// Intercept RDSAP request (used in global filter)
cy.intercept('GET', '/simulator-specification/systems/RDSAP', {
fixture: 'Dashboard/rdsap.json',
}).as('getRdSAP');
interceptDictionaries();
setConsentCookie();
},
setConsentCookie,
interceptDictionaries,
interceptMetadata,
interceptBuildingsMetadata,
interceptOpportunitiesMetadata,
Dashboard.cy.ts
import LoginPage from 'page-objects/Auth/loginPage';
import DashboardPageObjects from 'page-objects/dashboardPage';
import dashboardSavingsTestDataScenarios from 'fixtures/Dashboard/DataProviders/dashboardTestScenarios.json';
const {pathDashboard, interceptRequests, waitForInterception} =
DashboardPageObjects;
describe('Dashboard page', () => {
beforeEach(() => {
interceptRequests();
LoginPage.bypassLogin();
cy.visitURL(pathDashboard).then(() => {
waitForInterception();
});
});
it('Check dashboard table elements', () => {
cy.getByData(DashboardPageObjects.mainCard).containsMultiple(
'By our calculations you can save up to'
);
cy.getByData(DashboardPageObjects.infoCard).containsMultiple(
'Average Payback',
'Investment',
'Consumption',
'Emissions',
'Risk Scale'
);
cy.getByData(DashboardPageObjects.disaggregationChart).containsMultiple(
'What are the savings and where do they come from?',
'COST',
'CONSUMPTION [kWh]',
'EMISSIONS [kgCO e]',
'Heating',
'Cooling',
'HVAC Distribution',
'Lighting',
'Plug Loads',
'Domestic Hot Water',
'Fossil Fuel Plug Loads'
);
cy.getByData(DashboardPageObjects.potentialSavingsChart).containsMultiple(
'What buildings are driving the savings?',
'ENERGY SAVING POTENTIAL IN BUILDINGS (GWH/A) BY AVERAGE PAYBACK TIME (YEAR) AND USE',
'Simply Payback Time (years)',
'Energy Saved (GWh/A)'
);
});
describe('Toggle "What are savings and where do they come from?"', () => {
dashboardSavingsTestDataScenarios.forEach(testData => {
it(testData.testName, () => {
cy.get(DashboardPageObjects[testData.icon])
.eq(testData.iconIndex)
.click();
cy.get(DashboardPageObjects.charts).should(
'not.have.css',
'color',
testData.chartColor
);
cy.get(DashboardPageObjects[testData.icon]).should(
'have.length',
testData.iconLength
);
});
});
});
});
Additional Cypress Specs
NotificationBar.cy.ts
import CommonRequests from 'page-objects/Common/CommonRequests';
const visitAndCheckNotificationBar = () => {
cy.visit(`${Cypress.env('baseUrl')}`);
cy.get('[data-test="notification-bar"]').should('exist');
};
describe('NotificationBar', () => {
it('Notification bar is visible when feature flag is enabled', () => {
CommonRequests.interceptDictionaries();
cy.intercept('GET', '/feature-flags/feature-flags/KESTREL', {
body: [
{
key: 'NOTIFICATION_BAR',
active: true,
},
],
}).as('getFeatureFlags');
visitAndCheckNotificationBar();
});
it('Notification bar is hidden when feature flag is disabled', () => {
CommonRequests.interceptDictionaries();
cy.intercept('GET', '/feature-flags/feature-flags/KESTREL', {
body: [
{
key: 'NOTIFICATION_BAR',
active: false,
},
],
}).as('getFeatureFlags');
cy.visit(`${Cypress.env('baseUrl')}`);
cy.get('[data-test="notification-bar"]').should('not.exist');
});
it('Notification bar is visible when feature flags are failing', () => {
CommonRequests.interceptDictionaries();
cy.intercept('GET', '/feature-flags/feature-flags/KESTREL', {
statusCode: 401,
}).as('getFeatureFlags');
visitAndCheckNotificationBar();
});
it('Notification bar is visible when feature flags are missing', () => {
CommonRequests.interceptDictionaries();
cy.intercept('GET', '/feature-flags/feature-flags/KESTREL', {
body: [],
}).as('getFeatureFlags');
visitAndCheckNotificationBar();
});
});
Filters.cy.ts
import
import
import
import
import
import
import
LoginPage from 'page-objects/Auth/loginPage';
FiltersPageObjects from 'page-objects/filtersPage';
CommonSelectors from '../page-objects/Common/CommonSelectors';
DashboardPage from '../page-objects/dashboardPage';
CommonRequests from 'page-objects/Common/CommonRequests';
{test} from 'support/utils';
{Filter} from '../../src/types/Components/Filter';
const resetFilters = () => {
cy.get(FiltersPageObjects.resetFiltersButton).click();
cy.wait('@getSummary');
};
function applyFiltersAndSpyRequest() {
cy.realPress('Tab');
cy.getByData(FiltersPageObjects.applyFiltersButton).click();
return cy.wait('@getSummary').then(xhr => {
return xhr.request.body.filters as Array;
});
}
function getAndActivateFilterItem(selector) {
const filterItem = () =>
cy.getByData(`${FiltersPageObjects.filterItemPrefix}-${selector}`);
filterItem().getByData(FiltersPageObjects.filterLabel).click();
return filterItem;
}
describe('Filters', async () => {
it('Filters Flows', () => {
test('Initialize', () => {
DashboardPage.interceptRequests();
LoginPage.bypassLogin();
cy.visitURL('/dashboard').then(() => {
CommonRequests.waitForMetadata();
cy.wait('@getUserData');
cy.wait('@getPortfolios');
cy.wait('@getSummary');
cy.wait('@getSummedEstimations');
cy.wait('@getRdSAP');
});
});
test('Components are in place', () => {
// Check navigation if dashboard menu item is active
cy.get(CommonSelectors.navigation.dashboard).should(
'have.class',
'active'
);
// Check global filter if it contains all filter blocks
cy.getByData(FiltersPageObjects.filterItems).containsMultiple(
'Opportunity Filters',
'Building Filters',
'RdSAP Data',
'NWH Data'
);
});
Navigation.cy.ts
import
import
import
import
import
import
LoginPage from 'page-objects/Auth/loginPage';
CommonSelectors from '../page-objects/Common/CommonSelectors';
DashboardPage from '../page-objects/dashboardPage';
Organization from 'page-objects/Settings/organizationsPage';
CommonRequests from 'page-objects/Common/CommonRequests';
{group, test} from 'support/utils';
const toggleSettingsMenu = () => {
cy.get(CommonSelectors.navigation.settings_toggle).click({force: true});
};
describe('Navigation', async () => {
it('Navigation Flows', () => {
test('Initialize', () => {
CommonRequests.interceptMetadata();
DashboardPage.interceptRequests();
LoginPage.bypassLogin();
cy.visitURL('/dashboard').then(() => {
CommonRequests.waitForMetadata();
});
});
group('Top Bar Navigation', () => {
test('Buildings button redirects as expected', () => {
cy.get(CommonSelectors.navigation.buildings).click();
cy.location().should(location => {
expect(location.pathname).to.eq('/buildings');
});
});
test('Gross to Net button redirects as expected', () => {
cy.get(CommonSelectors.navigation.grossToNet).click();
cy.location().should(location => {
expect(location.pathname).to.eq('/gross-to-net');
});
});
test('Dashboard button redirects as expected', () => {
cy.get(CommonSelectors.navigation.buildings).click();
cy.get(CommonSelectors.navigation.dashboard).click();
cy.location().should(location => {
expect(location.pathname).to.eq('/dashboard');
});
});
test('Tallarna logo redirects to dashboard', () => {
cy.get(CommonSelectors.navigation.buildings).click();
cy.get(CommonSelectors.navigation.tallarnaLogo).click();
cy.location().should(location => {
expect(location.pathname).to.eq('/dashboard');
});
});
});
group('Settings Navigation', () => {
test('Initialize', () => {
Organization.interceptGetAccount();
toggleSettingsMenu();
cy.get(CommonSelectors.navigation.settings_dropdown_settings).click();
});
ChangePassword.cy.ts
import
import
import
import
ChangePassword from 'page-objects/Auth/changePassword';
Dashboard from 'page-objects/dashboardPage';
LoginPage from 'page-objects/Auth/loginPage';
{test} from 'support/utils';
describe('Change Password', () => {
it('All Flows', () => {
test('Initialize', () => {
Dashboard.interceptRequests();
LoginPage.bypassLogin();
cy.visitURL(Dashboard.pathDashboard).then(() => {
Dashboard.waitForInterception();
});
cy.get(ChangePassword.menuToggle).click();
cy.get(ChangePassword.menuItem).click();
cy.get(ChangePassword.changePasswordButton).should('be.disabled');
});
test('Error Validation', () => {
cy.typeFast(`${ChangePassword.newPasswordRow} input`, 'Abcdef0');
cy.get(ChangePassword.newPasswordValidationError).should('exist');
cy.typeFast(`${ChangePassword.repeatPasswordRow} input`, 'abd');
cy.get(ChangePassword.repeatPasswordValidationError).should('exist');
cy.get(ChangePassword.changePasswordButton).should('be.disabled');
});
test('Success Validation', () => {
cy.typeFast(`${ChangePassword.newPasswordRow} input`, 'Abcdef0.');
cy.get(ChangePassword.newPasswordValidationError).should('not.exist');
cy.typeFast(`${ChangePassword.repeatPasswordRow} input`, 'Abcdef0.');
cy.get(ChangePassword.repeatPasswordValidationError).should('not.exist');
cy.typeFast(`${ChangePassword.oldPasswordRow} input`, '123');
cy.get(ChangePassword.changePasswordButton).should('be.enabled');
});
test('Error Response', () => {
ChangePassword.interceptChangePasswordFailure();
cy.get(ChangePassword.changePasswordButton).click();
cy.validateToastNotification('Password change request failed.');
cy.get(ChangePassword.modal).should('be.visible');
});
test('Error Response', () => {
ChangePassword.interceptChangePassword();
cy.get(ChangePassword.changePasswordButton).click();
cy.wait('@changePassword').then(xhr => {
const requestBody = xhr.request.body;
expect(requestBody).to.have.property('password', '123');
expect(requestBody).to.have.property('new_password', 'Abcdef0.');
expect(requestBody).to.have.property(
'password_confirmation',
'Abcdef0.'
);
});
cy.validateToastNotification('Password has been changed successfully.');