Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OCT-2126 Projects E2E #612

Merged
merged 12 commits into from
Jan 16, 2025
340 changes: 340 additions & 0 deletions client/cypress/e2e/_21projects.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import chaiColors from 'chai-colors';

import {
connectWallet,
mockCoinPricesServer,
visitWithLoader,
checkProjectsViewLoaded,
getHeartedProjectsIndicator,
changeMainValueToCryptoToggle,
} from 'cypress/utils/e2e';
import { getNamesOfProjects } from 'cypress/utils/projects';
import viewports from 'cypress/utils/viewports';
import { QUERY_KEYS } from 'src/api/queryKeys';
import {
HAS_ONBOARDING_BEEN_CLOSED,
IS_CRYPTO_MAIN_VALUE_DISPLAY,
IS_ONBOARDING_DONE,
} from 'src/constants/localStorageKeys';
import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes';
import { ORDER_OPTIONS } from 'src/views/ProjectsView/utils';

import Chainable = Cypress.Chainable;

chai.use(chaiColors);

function checkProjectItemElements(
index: number,
name: string,
isPatronMode = false,
): Chainable<any> {
cy.get('[data-test^=ProjectsView__ProjectsListItem')
.eq(index)
.find('[data-test=ProjectsListItem__imageProfile]')
.should('be.visible');
cy.get('[data-test^=ProjectsView__ProjectsListItem]')
.eq(index)
.should('be.visible')
.find('[data-test=ProjectsListItem__name]')
.should('be.visible')
.contains(name);
cy.get('[data-test^=ProjectsView__ProjectsListItem')
.eq(index)
.find('[data-test=ProjectsListItem__IntroDescription]')
.should('be.visible');
cy.get('[data-test^=ProjectsView__ProjectsListItem')
.eq(index)
.find('[data-test=ProjectsListItem__ButtonAddToAllocate]')
.should('be.visible');

if (isPatronMode) {
cy.get('[data-test^=ProjectsView__ProjectsListItem')
.eq(index)
.find('[data-test=ProjectsListItem__ButtonAddToAllocate]')
.should('not.be.visible');
} else {
cy.get('[data-test^=ProjectsView__ProjectsListItem')
.eq(index)
.find('[data-test=ProjectsListItem__ButtonAddToAllocate]')
.should('be.visible');
}

return cy
.get('[data-test^=ProjectsView__ProjectsListItem')
.eq(index)
.find('[data-test=ProjectRewards]')
.should('be.visible');
}

function addProjectToAllocate(
index: number,
numberOfAddedProjects: number,
isNavbarVisible: boolean,
): Chainable<any> {
cy.get('[data-test^=ProjectsView__ProjectsListItem')
.eq(index)
.find('[data-test=ProjectsListItem__imageProfile]')
.should('be.visible');
cy.get('[data-test^=ProjectsView__ProjectsListItem')
.eq(index)
.find('[data-test=ProjectsListItem__IntroDescription]')
.should('be.visible');
cy.get('[data-test^=ProjectsView__ProjectsListItem')
.eq(index)
.find('[data-test=ProjectsListItem__ButtonAddToAllocate]')
.scrollIntoView();
cy.get('[data-test^=ProjectsView__ProjectsListItem')
.eq(index)
.find('[data-test=ProjectsListItem__ButtonAddToAllocate]')
.click();
cy.get('[data-test^=ProjectsView__ProjectsListItem')
.eq(index)
.find('[data-test=ProjectsListItem__ButtonAddToAllocate]')
.find('svg')
.find('path')
.then($el => $el.css('fill'))
.should('be.colored', '#FF6157');
cy.get('[data-test^=ProjectsView__ProjectsListItem')
.eq(index)
.find('[data-test=ProjectsListItem__ButtonAddToAllocate]')
.find('svg')
.find('path')
.then($el => $el.css('stroke'))
.should('be.colored', '#FF6157');
getHeartedProjectsIndicator(isNavbarVisible).contains(numberOfAddedProjects + 1);

if (isNavbarVisible) {
visitWithLoader(
ROOT_ROUTES.allocation.absolute,
isNavbarVisible ? ROOT_ROUTES.allocation.absolute : ROOT_ROUTES.home.absolute,
);
cy.get('[data-test=AllocationView]').should('be.visible');
} else {
cy.get('[data-test=LayoutTopBar__allocationButton]').click();
cy.get('[data-test=AllocationDrawer]').should('be.visible');
}
cy.get('[data-test=AllocationItem]').should('have.length', numberOfAddedProjects + 1);

return isNavbarVisible
? cy.go('back')
: cy.get('[data-test=AllocationDrawer__closeButton]').click();
}

function removeProjectFromAllocate(
numberOfProjects: number,
numberOfAddedProjects: number,
index: number,
isNavbarVisible: boolean,
): Chainable<any> {
cy.get('[data-test^=ProjectsView__ProjectsListItem')
.eq(index)
.find('[data-test=ProjectsListItem__ButtonAddToAllocate]')
.scrollIntoView();
cy.get('[data-test^=ProjectsView__ProjectsListItem')
.eq(index)
.find('[data-test=ProjectsListItem__ButtonAddToAllocate]')
.click();
if (isNavbarVisible) {
visitWithLoader(
ROOT_ROUTES.allocation.absolute,
isNavbarVisible ? ROOT_ROUTES.allocation.absolute : ROOT_ROUTES.home.absolute,
);
cy.get('[data-test=AllocationView]').should('be.visible');
} else {
cy.get('[data-test=LayoutTopBar__allocationButton]').click();
cy.get('[data-test=AllocationDrawer]').should('be.visible');
}
cy.get('[data-test=AllocationItem]').should('have.length', numberOfAddedProjects - 1);
if (index < numberOfProjects - 1) {
getHeartedProjectsIndicator(isNavbarVisible).contains(numberOfAddedProjects - 1);
} else {
getHeartedProjectsIndicator(isNavbarVisible).should('not.exist');
}

return isNavbarVisible
? cy.go('back')
: cy.get('[data-test=AllocationDrawer__closeButton]').click();
}

Object.values(viewports).forEach(
({ device, viewportWidth, viewportHeight, isMobile, isTablet }) => {
describe(`projects: ${device}`, { viewportHeight, viewportWidth }, () => {
let projectNames: string[] = [];

beforeEach(() => {
mockCoinPricesServer();
localStorage.setItem(IS_ONBOARDING_DONE, 'true');
localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true');
visitWithLoader(ROOT_ROUTES.projects.absolute);
checkProjectsViewLoaded();

/**
* This could be done in before hook, but CY wipes the state after each test
* (could be disabled, but creates other problems).
*
* Needs to be done for each test, because each has different default "random" order for projects.
*/
projectNames = getNamesOfProjects();
});

it('header is visible', () => {
cy.get('[data-test^=ProjectsView__ViewTitle]').should('be.visible');
});

it('user is able to see all the projects in the view', () => {
for (let i = 0; i < projectNames.length; i++) {
cy.get('[data-test^=ProjectsView__ProjectsListItem]').eq(i).scrollIntoView();
checkProjectItemElements(i, projectNames[i]);
}
});

it('user is able to add & remove the first and the last project to/from allocation, triggering change of the icon, change of the number in navbar', () => {
// This test checks the first and the last elements only to save time.
cy.get('[data-test=Navbar__numberOfAllocations]').should('not.exist');

addProjectToAllocate(0, 0, isMobile || isTablet);
addProjectToAllocate(projectNames.length - 1, 1, isMobile || isTablet);
removeProjectFromAllocate(projectNames.length, 2, 0, isMobile || isTablet);
removeProjectFromAllocate(
projectNames.length,
1,
projectNames.length - 1,
isMobile || isTablet,
);
});

it('user is able to add project to allocation in ProjectsView and remove it from allocation in AllocationView', () => {
cy.get('[data-test=Navbar__numberOfAllocations]').should('not.exist');
addProjectToAllocate(0, 0, isMobile || isTablet);

const isNavbarVisible = isMobile || isTablet;
if (isNavbarVisible) {
visitWithLoader(
ROOT_ROUTES.allocation.absolute,
isNavbarVisible ? ROOT_ROUTES.allocation.absolute : ROOT_ROUTES.home.absolute,
);
cy.get('[data-test=AllocationView]').should('be.visible');
} else {
cy.get('[data-test=LayoutTopBar__allocationButton]').click();
cy.get('[data-test=AllocationDrawer]').should('be.visible');
}

cy.get('[data-test=AllocationItemSkeleton]').should('not.exist');
cy.get('[data-test=AllocationItem]').then(el => {
const { x } = el[0].getBoundingClientRect();
cy.get('[data-test=AllocationItem]')
.trigger('pointerdown')
.trigger('pointermove', { pageX: x - 20 })
.trigger('pointerup', { pageX: x - 40 });
cy.wait(500);
cy.get('[data-test=AllocationItem__removeButton]').should('be.visible');
cy.get('[data-test=AllocationItem__removeButton]').click();
cy.get('[data-test=AllocationItem__removeButton]').should('not.exist');
cy.get('[data-test=AllocationItem]').should('not.exist');
cy.get('[data-test=Navbar__numberOfAllocations]').should('not.exist');
});
});

it(`shows current total (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: true)`, () => {
changeMainValueToCryptoToggle(!isMobile && !isTablet, 'crypto');
visitWithLoader(ROOT_ROUTES.projects.absolute);
cy.get('[data-test=ProjectRewards__currentTotal__number]')
.first()
.invoke('text')
.should('eq', '0 ETH');
});

it(`shows current total (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: false)`, () => {
changeMainValueToCryptoToggle(!isMobile && !isTablet, 'fiat');
visitWithLoader(ROOT_ROUTES.projects.absolute);

cy.get('[data-test=ProjectRewards__currentTotal__number]')
.first()
.invoke('text')
.should('eq', '$0.00');
});

it('every sorting option is clickable', () => {
// @ts-expect-error I don't want to define entire TFunction here.
const orderOptionsValues = ORDER_OPTIONS((key: string) => {}).map(element => element.value);
orderOptionsValues.forEach(orderOptionsValue => {
cy.get('[data-test=ProjectsView__InputSelect]').click();
cy.get(`[data-test=ProjectsView__InputSelect__Option--${orderOptionsValue}]`).click();
});
});

it('search field -- results should show project', () => {
cy.window().then(win => {
const { currentEpoch } = win.clientReactQuery.getQueryData(QUERY_KEYS.currentEpoch);

/**
* There may be two projects with the same part of their name.
* eg. "Ethereum A" and "Ethereum B" projects.
*
* In order to make sure only one is returned, we look for project name with only one word.
*/
const projectNameOneWord = projectNames.find(
projectName => projectName.split(' ').length === 1,
);

cy.get('[data-test=ProjectsList__InputText]')
.clear()
.type(`${projectNameOneWord} Epoch ${currentEpoch - 1}`);
cy.get('[data-test^=ProjectsSearchResults__ProjectsListItem]').should('have.length', 1);
});
});

it('search field -- no results should show no results image & text', () => {
cy.get('[data-test=ProjectsList__InputText]')
.clear()
.type('there-is-no-way-there-will-ever-be-a-project-with-such-a-name');
cy.get('[data-test=ProjectsSearchResults__noSearchResults]').should('be.visible');
cy.get('[data-test=ProjectsSearchResults__noSearchResults__Img]').should('be.visible');
});
});

describe(`projects (patron mode): ${device}`, { viewportHeight, viewportWidth }, () => {
let projectNames: string[] = [];

before(() => {
cy.clearLocalStorage();

/**
* Global Metamask setup done by Synpress is not always done.
* Since Synpress needs to have valid provider to fetch the data from contracts,
* setupMetamask is required in each test suite.
*/
cy.setupMetamask();
});

beforeEach(() => {
mockCoinPricesServer();
localStorage.setItem(IS_ONBOARDING_DONE, 'true');
localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true');
visitWithLoader(ROOT_ROUTES.projects.absolute);
connectWallet({ isPatronModeEnabled: true });
checkProjectsViewLoaded();

/**
* This could be done in before hook, but CY wipes the state after each test
* (could be disabled, but creates other problems).
*
* Needs to be done for each test, because each has different default "random" order for projects.
*/
projectNames = getNamesOfProjects();
});

after(() => {
cy.disconnectMetamaskWalletFromAllDapps();
});

it('button "add to allocate" is disabled', () => {
for (let i = 0; i < projectNames.length; i++) {
cy.get('[data-test^=ProjectsView__ProjectsListItem]').eq(i).scrollIntoView();
checkProjectItemElements(i, projectNames[i], true);
}
});
});
},
);
15 changes: 15 additions & 0 deletions client/cypress/utils/e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,18 @@ export const changeMainValueToCryptoToggle = (
);
});
};

export const getHeartedProjectsIndicator = (isNavbarVisible: boolean): Chainable<any> => {
return cy.get(
isNavbarVisible
? '[data-test=LayoutNavbar__numberOfAllocations]'
: '[data-test=LayoutTopBar__numberOfAllocations]',
);
};

export const checkHeartedProjectsIndicator = (
isNavbarVisible: boolean,
number = 1,
): Chainable<any> => {
return getHeartedProjectsIndicator(isNavbarVisible).contains(number);
};
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ const ProjectsSearchResults: FC<ProjectsSearchResultsProps> = ({
};

return (
<div className={styles.list}>
<div className={styles.list} data-test="ProjectsSearchResults">
{projectsIpfsWithRewardsAndEpochs.length === 0 && !isLoading && (
<div className={styles.noSearchResults}>
<div className={styles.noSearchResults} data-test="ProjectsSearchResults__noSearchResults">
<Img
className={styles.image}
dataTest="ProjectsList__noSearchResults__Img"
dataTest="ProjectsSearchResults__noSearchResults__Img"
src="images/swept.webp"
/>
{t('noSearchResults')}
Expand All @@ -62,8 +62,8 @@ const ProjectsSearchResults: FC<ProjectsSearchResultsProps> = ({
key={`${projectIpfsWithRewards.address}--${projectIpfsWithRewards.epoch}`}
dataTest={
projectIpfsWithRewards.epoch
? `ProjectsView__ProjectsListItem--archive--${index}`
: `ProjectsView__ProjectsListItem--${index}`
? `ProjectsSearchResults__ProjectsListItem--archive--${index}`
: `ProjectsSearchResults__ProjectsListItem--${index}`
}
epoch={epochNumberToFetchData}
projectIpfsWithRewards={projectIpfsWithRewards}
Expand Down
1 change: 1 addition & 0 deletions client/src/components/ui/InputSelect/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const INPUT_SELECT_VARIANTS = ['topselect', 'overselect', 'belowselect'] as cons
export type InputSelectVariants = (typeof INPUT_SELECT_VARIANTS)[number];

export interface Option {
dataTest?: string;
label: string;
value: string;
}
Expand Down
Loading
Loading