Upgrade Cypress and fix broken E2E tests

This commit is contained in:
ErickSkrauch committed 2026-10-02 19:42:36 +02:00
1 parent 3cbcf95d38
commit 147b924f49
31 files changed
+643 -1566

No files matched your search

-2
View File
@@ -51,8 +51,6 @@ module.exports = {
},
rules: {
'no-restricted-globals': 'off',
// Cypress plugins/index.js is a plain Node CommonJS file
'@typescript-eslint/no-require-imports': 'off',
// chai's `expect().to.be.true`-style assertions are bare member expressions
// (no call at the end), which this rule otherwise flags as a no-op
'@typescript-eslint/no-unused-expressions': 'off',
@@ -26,6 +26,8 @@ const changeContextSpringConfig = {
const { helpLinks: helpLinksStyles } = helpLinks;
const fieldsetStyles: CSSProperties = { border: 0, padding: 0, margin: 0, minWidth: 0 };
type PanelId = string;
/**
@@ -267,7 +269,7 @@ class PanelTransition extends React.PureComponent<Props, State> {
</MeasureHeight>
</div>
</Panel>
<div className={helpLinksStyles} data-testid="auth-controls-secondary">
<div className={helpLinksStyles}>
{panels.map((config) => this.getLinks(config))}
</div>
</Form>
@@ -368,7 +370,12 @@ class PanelTransition extends React.PureComponent<Props, State> {
}
if (length === 1) {
if (!this.wasAutoFocused) {
// Don't steal the focus from a field the user has already started typing into during the transition.
// The leaving panel's fields are disabled, so a focus that remained there doesn't count
const { activeElement } = document;
const isUserTyping = activeElement?.matches('input:enabled, textarea:enabled');
if (!this.wasAutoFocused && !isUserTyping) {
this.body?.autoFocus();
}
@@ -429,7 +436,12 @@ class PanelTransition extends React.PureComponent<Props, State> {
);
return (
<div key={`header/${key}`} style={transitionStyle}>
<div
key={`header/${key}`}
style={transitionStyle}
data-testid="auth-header"
{...this.getActiveAttrs(key)}
>
{hasBackButton ? backButton : null}
<div style={scrollStyle}>{Title}</div>
</div>
@@ -462,13 +474,22 @@ class PanelTransition extends React.PureComponent<Props, State> {
style={transitionStyle}
state={this.shouldMeasureHeight()}
onMeasure={(height) => this.onUpdateHeight(height, key)}
data-testid="auth-body"
{...this.getActiveAttrs(key)}
>
{React.cloneElement(Body, {
// @ts-ignore
ref: (body) => {
this.body = body;
},
})}
{this.renderFieldset(
key,
React.cloneElement(Body, {
// @ts-ignore
ref: (body) => {
// During the transition the leaving panel is still mounted and its ref may be called
// after the active one's, so take only the active panel's body
if (key === this.state.panelId) {
this.body = body;
}
},
}),
)}
</MeasureHeight>
);
}
@@ -479,7 +500,12 @@ class PanelTransition extends React.PureComponent<Props, State> {
const transitionStyle = this.getDefaultTransitionStyles(key, style as unknown as AnimationStyle);
return (
<div key={`footer/${key}`} style={transitionStyle}>
<div
key={`footer/${key}`}
style={transitionStyle}
data-testid="auth-controls"
{...this.getActiveAttrs(key)}
>
{Footer}
</div>
);
@@ -491,7 +517,12 @@ class PanelTransition extends React.PureComponent<Props, State> {
const transitionStyle = this.getDefaultTransitionStyles(key, style as unknown as AnimationStyle);
return (
<div key={`links/${key}`} style={transitionStyle}>
<div
key={`links/${key}`}
style={transitionStyle}
data-testid="auth-controls-secondary"
{...this.getActiveAttrs(key)}
>
{Links}
</div>
);
@@ -518,6 +549,31 @@ class PanelTransition extends React.PureComponent<Props, State> {
};
}
/**
* While a transition is running, the leaving panel is still mounted inside the same <form>. Disabling its
* fields excludes them from the form's validation and serialization, so a submit made during the transition
* isn't blocked by the required fields of the previous panel.
*
* This is not probably what happens in the real life, but E2E tests works too fast and constantly bugs with that
*/
renderFieldset(key: PanelId, children: ReactNode): ReactElement {
return (
<fieldset disabled={key !== this.state.panelId} style={fieldsetStyles}>
{children}
</fieldset>
);
}
/**
* This helps E2E correctly select elements skipping those, that are still presented in the DOM during
* panel transition to the next one.
*/
getActiveAttrs(key: PanelId): Record<string, string> {
return {
'data-e2e-panel-active': key === this.state.panelId ? 'true' : 'false',
};
}
translate(value: number, direction: 'X' | 'Y' = 'X', unit: '%' | 'px' = '%'): CSSProperties {
return {
WebkitTransform: `translate${direction}(${value}${unit})`,
+3 -3
View File
@@ -30,7 +30,7 @@ export const Panel: FC<PanelProps> = ({ title, icon, children }) => {
export const PanelHeader: FC<PropsWithChildren<any>> = ({ children }) => {
return (
<div className={styles.header} data-testid="auth-header">
<div className={styles.header}>
{children}
</div>
);
@@ -38,7 +38,7 @@ export const PanelHeader: FC<PropsWithChildren<any>> = ({ children }) => {
export const PanelBody: FC<PropsWithChildren<any>> = ({ children }) => {
return (
<div className={styles.body} data-testid="auth-body">
<div className={styles.body}>
{children}
</div>
);
@@ -46,7 +46,7 @@ export const PanelBody: FC<PropsWithChildren<any>> = ({ children }) => {
export const PanelFooter: FC<PropsWithChildren<any>> = ({ children }) => {
return (
<div className={styles.footer} data-testid="auth-controls">
<div className={styles.footer}>
{children}
</div>
);
+9 -1
View File
@@ -2,7 +2,7 @@ import React from 'react';
import clsx from 'clsx';
import { CaptchaID } from 'app/services/captcha';
import { Skin } from 'app/components/ui';
import captcha from 'app/services/captcha';
import captcha, { registerE2eCallback } from 'app/services/captcha';
import logger from 'app/services/logger';
import { ComponentLoader } from 'app/components/ui/loader';
@@ -26,7 +26,11 @@ export default class Captcha extends FormInputComponent<
delay: 0,
};
unregisterE2eCallback?: () => void;
componentDidMount() {
this.unregisterE2eCallback = registerE2eCallback(this.setCode);
setTimeout(() => {
const { current: el } = this.elRef;
@@ -47,6 +51,10 @@ export default class Captcha extends FormInputComponent<
}, this.props.delay);
}
componentWillUnmount() {
this.unregisterE2eCallback?.();
}
render() {
const { skin } = this.props;
+13 -5
View File
@@ -7,6 +7,19 @@ let sitekey: string;
export type CaptchaID = string;
// For E2E testing purposes only: lets tests pass the captcha by calling `window.e2eCaptchaSetCode(code)`.
// The code is passed to every mounted captcha, so a test doesn't depend on which one of them (e.g. during
// a panel transition) was rendered last
const e2eCallbacks = new Set<(code: string) => void>();
(window as any).e2eCaptchaSetCode = (code: string) => e2eCallbacks.forEach((callback) => callback(code));
export function registerE2eCallback(callback: (code: string) => void): () => void {
e2eCallbacks.add(callback);
return () => e2eCallbacks.delete(callback);
}
class Captcha {
/**
* @param {DOMNode|string} el - dom node or id of element where to render captcha
@@ -27,9 +40,6 @@ class Captcha {
onSetCode: (code: string) => void;
},
): Promise<CaptchaID> {
// for testing purposes only
(window as any).e2eCaptchaSetCode = callback;
return this.loadApi().then(() =>
(window as any).grecaptcha.render(el, {
sitekey,
@@ -43,8 +53,6 @@ class Captcha {
* @param {string} captchaId - captcha id, returned from render promise
*/
reset(captchaId: CaptchaID) {
delete (window as any).e2eCaptchaSetCode;
this.loadApi().then(() => (window as any).grecaptcha.reset(captchaId));
}
+15
View File
@@ -0,0 +1,15 @@
import { defineConfig } from 'cypress';
export default defineConfig({
projectId: 'uftjbg',
chromeWebSecurity: false,
// Cypress 16's native network interception in Chromium-based browsers randomly hangs multi-spec runs.
// Route them through the legacy path until it's fixed: https://github.com/cypress-io/cypress/issues/34969
forceHttp1: true,
e2e: {
baseUrl: 'http://localhost:8080',
specPattern: 'cypress/e2e/**/*.test.ts',
// Restores the "Run X specs" button in the spec list (removed by default in Cypress 10+)
experimentalRunAllSpecs: true,
},
});
-6
View File
@@ -1,6 +0,0 @@
{
"baseUrl": "http://localhost:8080",
"testFiles": ["**/*.test.ts"],
"chromeWebSecurity": false,
"projectId": "uftjbg"
}
@@ -5,15 +5,10 @@ describe('Forgot / reset password', () => {
const captchaCode = 'captchaCode';
const emailMask = 'fo*@gm*l.**m';
cy.server();
cy.route({
method: 'POST',
url: '/api/authentication/forgot-password',
response: {
success: true,
data: {
emailMask,
},
cy.intercept('POST', '/api/authentication/forgot-password', {
success: true,
data: {
emailMask,
},
}).as('forgot');
@@ -35,10 +30,11 @@ describe('Forgot / reset password', () => {
// @ts-ignore
win.e2eCaptchaSetCode(captchaCode);
});
cy.get('[type=submit]').should('have.length', 1).click();
cy.get('[type=submit]').click();
cy.wait('@forgot')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -57,15 +53,10 @@ describe('Forgot / reset password', () => {
const login = 'foo';
const emailMask = 'fo*@gm*l.**m';
cy.server();
cy.route({
method: 'POST',
url: '/api/authentication/forgot-password',
response: {
success: true,
data: {
emailMask,
},
cy.intercept('POST', '/api/authentication/forgot-password', {
success: true,
data: {
emailMask,
},
}).as('forgot');
@@ -89,10 +80,11 @@ describe('Forgot / reset password', () => {
// @ts-ignore
win.e2eCaptchaSetCode(captchaCode);
});
cy.get('[type=submit]').should('have.length', 1).click();
cy.get('[type=submit]').click();
cy.wait('@forgot')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -109,15 +101,10 @@ describe('Forgot / reset password', () => {
const login = 'foo';
const emailMask = 'fo*@gm*l.**m';
cy.server();
cy.route({
method: 'POST',
url: '/api/authentication/forgot-password',
response: {
success: true,
data: {
emailMask,
},
cy.intercept('POST', '/api/authentication/forgot-password', {
success: true,
data: {
emailMask,
},
}).as('forgot');
@@ -130,10 +117,10 @@ describe('Forgot / reset password', () => {
// @ts-ignore
win.e2eCaptchaSetCode(captchaCode);
});
cy.get('[type=submit]').should('have.length', 1).click();
cy.get('[type=submit]').click();
cy.wait('@forgot')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -149,17 +136,12 @@ describe('Forgot / reset password', () => {
const key = 'key';
const newPassword = 'newPassword';
cy.server();
cy.login({
accounts: ['default'],
updateState: false,
rawApiResp: true,
}).then(({ accounts: [account] }) => {
cy.route({
method: 'POST',
url: '/api/authentication/recover-password',
response: account,
}).as('recover');
cy.intercept('POST', '/api/authentication/recover-password', account).as('recover');
});
cy.visit('/');
@@ -184,10 +166,10 @@ describe('Forgot / reset password', () => {
cy.get('[name=key]').type(key);
cy.get('[name=newPassword]').type(newPassword);
cy.get('[name=newRePassword]').type(newPassword);
cy.get('[type=submit]').should('have.length', 1).click();
cy.get('[type=submit]').click();
cy.wait('@recover')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -202,17 +184,12 @@ describe('Forgot / reset password', () => {
const key = 'key';
const newPassword = 'newPassword';
cy.server();
cy.login({
accounts: ['default'],
updateState: false,
rawApiResp: true,
}).then(({ accounts: [account] }) => {
cy.route({
method: 'POST',
url: '/api/authentication/recover-password',
response: account,
}).as('recover');
cy.intercept('POST', '/api/authentication/recover-password', account).as('recover');
});
cy.visit('/');
@@ -223,10 +200,10 @@ describe('Forgot / reset password', () => {
cy.get('[name=key]').should('have.attr', 'readonly');
cy.get('[name=newPassword]').type(newPassword);
cy.get('[name=newRePassword]').type(newPassword);
cy.get('[type=submit]').should('have.length', 1).click();
cy.get('[type=submit]').click();
cy.wait('@recover')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -80,18 +80,12 @@ describe('User with invalid token and refreshToken', () => {
});
it('should allow logout', () => {
cy.server();
cy.route({
url: `/api/v1/accounts/${account2.id}`,
}).as('account');
cy.route({
method: 'POST',
url: '/api/authentication/logout',
}).as('logout');
cy.intercept(`/api/v1/accounts/${account2.id}`).as('account');
cy.intercept('POST', '/api/authentication/logout').as('logout');
cy.visit('/');
cy.wait('@account').its('status').should('eq', 401);
cy.wait('@account').its('response.statusCode').should('eq', 401);
cy.findByTestId('toolbar').contains(account2.username).click();
cy.findByTestId('toolbar').contains('Log out').click();
@@ -102,20 +96,16 @@ describe('User with invalid token and refreshToken', () => {
});
it('should allow enter new login from choose account', () => {
cy.server();
cy.route({
url: `/api/v1/accounts/${account2.id}`,
}).as('account');
cy.intercept(`/api/v1/accounts/${account2.id}`).as('account');
cy.visit('/');
cy.wait('@account').its('status').should('eq', 401);
cy.wait('@account').its('response.statusCode').should('eq', 401);
cy.url().should('include', '/password');
cy.get('[data-e2e-go-back]').click();
cy.get('[name=password]').should('not.exist'); // wait till panel transition end
cy.url().should('include', '/choose-account');
cy.contains('Log into another').click();
@@ -128,7 +118,6 @@ describe('User with invalid token and refreshToken', () => {
cy.get('[name=password]').type(account1.password);
cy.get('[name=rememberMe]').should('be.checked');
cy.get('[type=submit]').should('have.length', 1); // wait till transition ends
cy.get('[type=submit]').click();
cy.location('pathname').should('eq', '/');
@@ -160,9 +149,6 @@ describe('User with invalid token and refreshToken', () => {
cy.url().should('include', '/password');
// TODO: remove wait and fix logic so that
// it won't show 'Please enter E‑mail or username' error
cy.wait(1000);
cy.get('[name="password"]').type(`${account1.password}{enter}`);
cy.location('pathname').should('eq', '/');
@@ -189,7 +175,6 @@ describe('User with invalid token and refreshToken', () => {
cy.url().should('contain', '/password');
cy.get('[data-e2e-go-back]').click();
cy.get('[name=password]').should('not.exist'); // wait till panel transition end
cy.contains('[type=submit]', 'Log into another account').click();
cy.contains('a', 'Create new account').click();
@@ -44,11 +44,7 @@ describe('OAuth', () => {
describe('static pages', () => {
it('should authenticate using static page', () => {
cy.server();
cy.route({
method: 'POST',
url: '/api/oauth2/v1/complete**',
}).as('complete');
cy.intercept('POST', '/api/oauth2/v1/complete**').as('complete');
cy.login({ accounts: ['default'] });
@@ -66,11 +62,7 @@ describe('OAuth', () => {
});
it('should authenticate using static page with code', () => {
cy.server();
cy.route({
method: 'POST',
url: '/api/oauth2/v1/complete**',
}).as('complete');
cy.intercept('POST', '/api/oauth2/v1/complete**').as('complete');
cy.login({ accounts: ['default'] });
@@ -200,19 +192,15 @@ describe('OAuth', () => {
describe('Permissions prompt', () => {
// TODO: remove api mocks, when we will be able to revoke permissions
it('should prompt for permissions', () => {
cy.server();
cy.route({
method: 'POST',
cy.intercept('POST', new RegExp('/api/oauth2/v1/complete'), {
// NOTE: can not use cypress glob syntax, because it will break due to
// '%2F%2F' (//) in redirect_uri
// url: '/api/oauth2/v1/complete/*',
url: new RegExp('/api/oauth2/v1/complete'),
response: {
// '/api/oauth2/v1/complete/*',
statusCode: 401,
body: {
statusCode: 401,
error: 'accept_required',
},
status: 401,
}).as('complete');
cy.login({ accounts: ['default'] });
@@ -230,7 +218,8 @@ describe('OAuth', () => {
assertPermissions();
cy.server({ enable: false });
// stop stubbing so the click below hits the real backend
cy.intercept('POST', new RegExp('/api/oauth2/v1/complete'), (req) => req.continue());
cy.findByTestId('auth-controls').contains('Approve').click();
@@ -238,19 +227,15 @@ describe('OAuth', () => {
});
it('should redirect to error page, when permission request declined', () => {
cy.server();
cy.route({
method: 'POST',
cy.intercept('POST', new RegExp('/api/oauth2/v1/complete'), {
// NOTE: can not use cypress glob syntax, because it will break due to
// '%2F%2F' (//) in redirect_uri
// url: '/api/oauth2/v1/complete/*',
url: new RegExp('/api/oauth2/v1/complete'),
response: {
// '/api/oauth2/v1/complete/*',
statusCode: 401,
body: {
statusCode: 401,
error: 'accept_required',
},
status: 401,
}).as('complete');
cy.login({ accounts: ['default'] });
@@ -267,7 +252,8 @@ describe('OAuth', () => {
assertPermissions();
cy.server({ enable: false });
// stop stubbing so the click below hits the real backend
cy.intercept('POST', new RegExp('/api/oauth2/v1/complete'), (req) => req.continue());
cy.findByTestId('auth-controls-secondary').contains('Decline').click();
@@ -342,25 +328,16 @@ describe('OAuth', () => {
cy.url().should('include', '/password');
cy.server();
cy.route({
method: 'POST',
url: '/api/authentication/login',
response: {
success: false,
errors: { totp: 'error.totp_required' },
},
cy.intercept('POST', '/api/authentication/login', {
success: false,
errors: { totp: 'error.totp_required' },
});
cy.get('[name=password]').type(`${account1.password}{enter}`);
cy.url().should('include', '/mfa');
// Undo request response mock
cy.route({
method: 'POST',
url: '/api/authentication/login',
});
cy.intercept('POST', '/api/authentication/login', (req) => req.continue());
cy.get('[name=totp]').type('123456{enter}');
@@ -371,26 +348,21 @@ describe('OAuth', () => {
describe('Deleted account', () => {
it('should show account switcher and then abort oauth and redirect to profile', () => {
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.server();
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account1.id}`,
response: {
id: account.id,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: account.username,
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: account.email,
isActive: true,
isDeleted: true, // force user into the deleted state
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse,
});
cy.intercept('GET', `/api/v1/accounts/${account1.id}`, {
id: account.id,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: account.username,
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: account.email,
isActive: true,
isDeleted: true, // force user into the deleted state
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse);
cy.visit(`/oauth2/v1/ely?${new URLSearchParams(defaults)}`);
@@ -412,26 +384,21 @@ describe('OAuth', () => {
cy.url().should('include', '/password');
cy.server();
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account1.id}`,
response: {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: true, // force user into the deleted state
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse,
});
cy.intercept('GET', `/api/v1/accounts/${account1.id}`, {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: true, // force user into the deleted state
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse);
cy.get('[name=password]').type(`${account1.password}{enter}`);
@@ -6,24 +6,15 @@ describe('Register', () => {
const captchaCode = 'captchaCode';
const activationKey = 'activationKey';
cy.server();
cy.route({
method: 'POST',
url: '/api/signup',
response: {
success: true,
},
cy.intercept('POST', '/api/signup', {
success: true,
}).as('signup');
cy.login({
accounts: ['default'],
updateState: false,
rawApiResp: true,
}).then(({ accounts: [account] }) => {
cy.route({
method: 'POST',
url: '/api/signup/confirm',
response: account,
}).as('activate');
cy.intercept('POST', '/api/signup/confirm', account).as('activate');
});
cy.visit('/');
@@ -47,7 +38,7 @@ describe('Register', () => {
cy.get('[type=submit]').click();
cy.wait('@signup')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -65,7 +56,7 @@ describe('Register', () => {
cy.get('[name=key]').type(`${activationKey}{enter}`);
cy.wait('@activate').its('requestBody').should('eq', `key=${activationKey}`);
cy.wait('@activate').its('request.body').should('eq', `key=${activationKey}`);
cy.location('pathname').should('eq', '/');
});
@@ -76,13 +67,8 @@ describe('Register', () => {
const password = String(Date.now());
const captchaCode = 'captchaCode';
cy.server();
cy.route({
method: 'POST',
url: '/api/signup',
response: {
success: true,
},
cy.intercept('POST', '/api/signup', {
success: true,
}).as('signup');
cy.login({
accounts: ['default'],
@@ -122,17 +108,12 @@ describe('Register', () => {
it('should allow activation', () => {
const activationKey = 'activationKey';
cy.server();
cy.login({
accounts: ['default'],
updateState: false,
rawApiResp: true,
}).then(({ accounts: [account] }) => {
cy.route({
method: 'POST',
url: '/api/signup/confirm',
response: account,
}).as('activate');
cy.intercept('POST', '/api/signup/confirm', account).as('activate');
});
cy.visit('/register');
@@ -142,7 +123,7 @@ describe('Register', () => {
cy.get('[name=key]').type(`${activationKey}{enter}`);
cy.wait('@activate').its('requestBody').should('eq', `key=${activationKey}`);
cy.wait('@activate').its('request.body').should('eq', `key=${activationKey}`);
cy.location('pathname').should('eq', '/');
});
@@ -150,17 +131,12 @@ describe('Register', () => {
it('should read activation key from url', () => {
const activationKey = 'activationKey';
cy.server();
cy.login({
accounts: ['default'],
updateState: false,
rawApiResp: true,
}).then(({ accounts: [account] }) => {
cy.route({
method: 'POST',
url: '/api/signup/confirm',
response: account,
}).as('activate');
cy.intercept('POST', '/api/signup/confirm', account).as('activate');
});
cy.visit(`/activation/${activationKey}`);
@@ -168,7 +144,7 @@ describe('Register', () => {
cy.get('[name=key]').should('have.attr', 'readonly');
cy.get('[type=submit]').click();
cy.wait('@activate').its('requestBody').should('eq', `key=${activationKey}`);
cy.wait('@activate').its('request.body').should('eq', `key=${activationKey}`);
cy.location('pathname').should('eq', '/');
});
@@ -177,12 +153,7 @@ describe('Register', () => {
const email = `${Date.now()}@gmail.com`;
const captchaCode = 'captchaCode';
cy.server();
cy.route({
method: 'POST',
url: '/api/signup/repeat-message',
response: { success: true },
}).as('resend');
cy.intercept('POST', '/api/signup/repeat-message', { success: true }).as('resend');
cy.visit('/register');
cy.findByTestId('auth-controls-secondary').contains('not received').click();
@@ -199,7 +170,7 @@ describe('Register', () => {
cy.get('[type=submit]').click();
cy.wait('@resend')
.its('requestBody')
.its('request.body')
.should('eq', new URLSearchParams({ email, captcha: captchaCode }).toString());
cy.location('pathname').should('eq', '/activation');
@@ -93,14 +93,9 @@ describe('Sign in / Log out', () => {
cy.url().should('include', '/password');
cy.server();
cy.route({
method: 'POST',
url: '/api/authentication/login',
response: {
success: false,
errors: { totp: 'error.totp_required' },
},
cy.intercept('POST', '/api/authentication/login', {
success: false,
errors: { totp: 'error.totp_required' },
});
cy.get('[name=password]').type(account1.password);
@@ -108,14 +103,11 @@ describe('Sign in / Log out', () => {
cy.url().should('include', '/mfa');
cy.route({
method: 'POST',
url: '/api/authentication/login',
}).as('login');
cy.intercept('POST', '/api/authentication/login', (req) => req.continue()).as('login');
cy.get('[name=totp]').type('123{enter}');
cy.wait('@login').its('requestBody').should('include', 'totp=123');
cy.wait('@login').its('request.body').should('include', 'totp=123');
cy.location('pathname').should('eq', '/');
});
@@ -143,37 +135,29 @@ describe('Sign in / Log out', () => {
cy.get('[name=password]').type(account1.password);
cy.get('[name=rememberMe]').should('be.checked');
cy.server();
cy.route({
method: 'POST',
url: `/api/v1/accounts/${account1.id}/rules`,
}).as('rulesAgreement');
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account1.id}`,
response: {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: false,
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: true, // force user to accept updated user agreement
} as UserResponse,
});
cy.intercept('POST', `/api/v1/accounts/${account1.id}/rules`).as('rulesAgreement');
cy.intercept('GET', `/api/v1/accounts/${account1.id}`, {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: false,
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: true, // force user to accept updated user agreement
} as UserResponse);
cy.get('[type=submit]').click();
cy.location('pathname').should('eq', '/accept-rules');
cy.get('[type=submit]').last().click(); // add .last() to match the new state during its transition
cy.wait('@rulesAgreement').its('requestBody').should('be.empty');
cy.get('[type=submit]').click();
cy.wait('@rulesAgreement').its('request.body').should('be.empty');
cy.location('pathname').should('eq', '/');
cy.findByTestId('profile-index').should('contain', account1.username);
@@ -189,26 +173,21 @@ describe('Sign in / Log out', () => {
cy.get('[name=password]').type(account1.password);
cy.get('[name=rememberMe]').should('be.checked');
cy.server();
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account1.id}`,
response: {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: false,
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: true, // force user to accept updated user agreement
} as UserResponse,
});
cy.intercept('GET', `/api/v1/accounts/${account1.id}`, {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: false,
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: true, // force user to accept updated user agreement
} as UserResponse);
cy.get('[type=submit]').click();
@@ -230,26 +209,21 @@ describe('Sign in / Log out', () => {
cy.get('[name=password]').type(account1.password);
cy.get('[name=rememberMe]').should('be.checked');
cy.server();
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account1.id}`,
response: {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: false,
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: true, // force user to accept updated user agreement
} as UserResponse,
});
cy.intercept('GET', `/api/v1/accounts/${account1.id}`, {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: false,
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: true, // force user to accept updated user agreement
} as UserResponse);
cy.get('[type=submit]').click();
@@ -259,34 +233,27 @@ describe('Sign in / Log out', () => {
cy.location('pathname').should('eq', '/profile/delete');
cy.route({
method: 'DELETE',
url: `/api/v1/accounts/${account1.id}`,
}).as('deleteAccount');
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account1.id}`,
response: {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: true, // mock deleted state since the delete will not perform the real request
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: true, // rules still aren't accepted
} as UserResponse,
});
cy.intercept('DELETE', `/api/v1/accounts/${account1.id}`).as('deleteAccount');
cy.intercept('GET', `/api/v1/accounts/${account1.id}`, {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: true, // mock deleted state since the delete will not perform the real request
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: true, // rules still aren't accepted
} as UserResponse);
cy.get('[type=submit]').click();
cy.wait('@deleteAccount')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -294,16 +261,12 @@ describe('Sign in / Log out', () => {
}).toString(),
);
cy.route({
method: 'DELETE',
url: `/api/v1/accounts/${account1.id}`,
response: { success: true },
}).as('deleteAccount');
cy.intercept('DELETE', `/api/v1/accounts/${account1.id}`, { success: true }).as('deleteAccount');
confirmWithPassword(account1.password);
cy.wait('@deleteAccount')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -17,19 +17,9 @@ describe('Applications', () => {
});
it('should add website app', () => {
cy.server();
cy.route({
method: 'POST',
url: '/api/v1/oauth2/*/reset',
}).as('revoke');
cy.route({
method: 'DELETE',
url: '/api/v1/oauth2/*',
}).as('delete');
cy.route({
method: 'POST',
url: '/api/v1/oauth2/*/reset?regenerateSecret',
}).as('revokeSecret');
cy.intercept('POST', '/api/v1/oauth2/*/reset').as('revoke');
cy.intercept('DELETE', '/api/v1/oauth2/*').as('delete');
cy.intercept('POST', '/api/v1/oauth2/*/reset?regenerateSecret').as('revokeSecret');
cy.visit('/dev/applications');
@@ -66,7 +56,7 @@ describe('Applications', () => {
// test reset client secret
cy.findByTestId('client-secret').then(([el]) => {
const prevSecret = el.value;
const prevSecret = (el as HTMLInputElement).value;
cy.findByTestId('client-secret').should('have.value', prevSecret);
cy.contains('Reset Client Secret').click();
@@ -6,21 +6,11 @@ describe('Profile — Change Email', () => {
const key2 = 'key1232';
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.server();
cy.route({
method: 'POST',
url: `/api/v1/accounts/${account.id}/email-verification`,
}).as('requestEmailChange');
cy.route({
method: 'POST',
url: `/api/v1/accounts/${account.id}/new-email-verification`,
response: { success: true },
cy.intercept('POST', `/api/v1/accounts/${account.id}/email-verification`).as('requestEmailChange');
cy.intercept('POST', `/api/v1/accounts/${account.id}/new-email-verification`, {
success: true,
}).as('verifyNewEmail');
cy.route({
method: 'POST',
url: `/api/v1/accounts/${account.id}/email`,
response: { success: true },
}).as('saveEmail');
cy.intercept('POST', `/api/v1/accounts/${account.id}/email`, { success: true }).as('saveEmail');
cy.visit('/');
@@ -31,7 +21,7 @@ describe('Profile — Change Email', () => {
cy.contains('Send E‑mail').click();
cy.wait('@requestEmailChange')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -39,16 +29,14 @@ describe('Profile — Change Email', () => {
}).toString(),
);
cy.route({
method: 'POST',
url: `/api/v1/accounts/${account.id}/email-verification`,
response: { success: true },
}).as('requestEmailChange');
cy.intercept('POST', `/api/v1/accounts/${account.id}/email-verification`, { success: true }).as(
'requestEmailChange',
);
confirmWithPassword(account.password);
cy.wait('@requestEmailChange')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -63,7 +51,7 @@ describe('Profile — Change Email', () => {
cy.findByTestId('step2').find('[name=email]').type(`${account.email}{enter}`);
cy.wait('@verifyNewEmail')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -78,7 +66,7 @@ describe('Profile — Change Email', () => {
cy.findByTestId('step3').find('[name=key]').type(`${key2}{enter}`);
cy.wait('@saveEmail')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -107,11 +95,8 @@ describe('Profile — Change Email', () => {
const key = 'key123';
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.server();
cy.route({
method: 'POST',
url: `/api/v1/accounts/${account.id}/new-email-verification`,
response: { success: true },
cy.intercept('POST', `/api/v1/accounts/${account.id}/new-email-verification`, {
success: true,
}).as('verifyNewEmail');
cy.visit(`/profile/change-email/step2/${key}`);
@@ -121,7 +106,7 @@ describe('Profile — Change Email', () => {
cy.findByTestId('step2').find('[name=email]').type(`${account.email}{enter}`);
cy.wait('@verifyNewEmail')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -138,12 +123,7 @@ describe('Profile — Change Email', () => {
const key = 'key123';
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.server();
cy.route({
method: 'POST',
url: `/api/v1/accounts/${account.id}/email`,
response: { success: true },
}).as('saveEmail');
cy.intercept('POST', `/api/v1/accounts/${account.id}/email`, { success: true }).as('saveEmail');
cy.visit(`/profile/change-email/step3/${key}`);
@@ -152,7 +132,7 @@ describe('Profile — Change Email', () => {
cy.findByTestId('change-email').find('[type=submit]').click();
cy.wait('@saveEmail').its('requestBody').should(
cy.wait('@saveEmail').its('request.body').should(
'eq',
new URLSearchParams({
key,
@@ -3,11 +3,7 @@ import { openSectionByName, confirmWithPassword } from './utils';
describe('Profile — Change password', () => {
it('should change password', () => {
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.server();
cy.route({
method: 'POST',
url: `/api/v1/accounts/${account.id}/password`,
}).as('password');
cy.intercept('POST', `/api/v1/accounts/${account.id}/password`).as('password');
cy.visit('/');
openSectionByName('Password');
@@ -20,7 +16,7 @@ describe('Profile — Change password', () => {
cy.get('[type=submit]').click();
cy.wait('@password')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -34,7 +30,7 @@ describe('Profile — Change password', () => {
confirmWithPassword(account.password);
cy.wait('@password')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -51,15 +47,10 @@ describe('Profile — Change password', () => {
it('should close password popup if form has errors', () => {
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.server();
cy.route({
method: 'POST',
url: `/api/v1/accounts/${account.id}/password`,
response: {
success: false,
errors: {
password: 'force popup to be shown',
},
cy.intercept('POST', `/api/v1/accounts/${account.id}/password`, {
success: false,
errors: {
password: 'force popup to be shown',
},
}).as('password');
cy.visit('/');
@@ -75,10 +66,7 @@ describe('Profile — Change password', () => {
cy.get('[type=submit]').click();
// disable response mocks
cy.route({
method: 'POST',
url: `/api/v1/accounts/${account.id}/password`,
}).as('password');
cy.intercept('POST', `/api/v1/accounts/${account.id}/password`, (req) => req.continue()).as('password');
confirmWithPassword(account.password);
@@ -4,33 +4,24 @@ import { openSectionByName, confirmWithPassword } from './utils';
describe('Profile — Change Username', () => {
it('should change username', () => {
cy.server();
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account.id}`,
response: {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'FooBar',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: false,
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse,
});
cy.intercept('GET', `/api/v1/accounts/${account.id}`, {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'FooBar',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: false,
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse);
cy.route({
method: 'POST',
url: `/api/v1/accounts/${account.id}/username`,
}).as('username');
cy.intercept('POST', `/api/v1/accounts/${account.id}/username`).as('username');
cy.visit('/');
@@ -41,13 +32,10 @@ describe('Profile — Change Username', () => {
cy.get('[name=username]').type(`{selectall}${account.username}{enter}`);
// unmock accounts route
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account.id}`,
});
cy.intercept('GET', `/api/v1/accounts/${account.id}`, (req) => req.continue());
cy.wait('@username')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -59,7 +47,7 @@ describe('Profile — Change Username', () => {
confirmWithPassword(account.password);
cy.wait('@username')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -0,0 +1,60 @@
import { openSectionByName, confirmWithPassword } from './utils';
import { UserResponse } from 'app/services/api/accounts';
describe('Profile — Delete account', () => {
it('should delete account', () => {
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.intercept('DELETE', `/api/v1/accounts/${account.id}`).as('deleteAccount');
cy.intercept('GET', `/api/v1/accounts/${account.id}`);
cy.visit('/');
openSectionByName('Account deletion');
cy.location('pathname').should('eq', '/profile/delete');
cy.get('[type=submit]').click();
cy.wait('@deleteAccount')
.its('request.body')
.should(
'eq',
new URLSearchParams({
password: '',
}).toString(),
);
cy.intercept('DELETE', `/api/v1/accounts/${account.id}`, { success: true }).as('deleteAccount');
cy.intercept('GET', `/api/v1/accounts/${account.id}`, {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: true, // mock deleted state since the delete will not perform the real request
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse);
confirmWithPassword(account.password);
cy.wait('@deleteAccount')
.its('request.body')
.should(
'eq',
new URLSearchParams({
password: account.password,
}).toString(),
);
cy.location('pathname').should('eq', '/');
cy.findByTestId('deletedAccount').should('contain', 'Account is deleted');
});
});
});
@@ -5,12 +5,7 @@ describe('feedback popup', () => {
const subject = 'Hello world';
const message = 'This is a feedback message';
cy.server();
cy.route({
url: '/api/feedback',
method: 'POST',
response: { success: true },
});
cy.intercept('POST', '/api/feedback', { success: true });
cy.login({ accounts: ['default'] });
@@ -24,12 +24,7 @@ describe('Change locale', () => {
it('should change locale from the profile', () => {
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.server();
cy.route({
method: 'POST',
url: `/api/v1/accounts/${account.id}/language`,
response: { success: true },
}).as('language');
cy.intercept('POST', `/api/v1/accounts/${account.id}/language`, { success: true }).as('language');
});
cy.visit('/');
@@ -41,7 +36,7 @@ describe('Change locale', () => {
cy.findByTestId('languages-list-item').contains('Belarusian').click();
cy.wait('@language').its('requestBody').should('eq', 'lang=be');
cy.wait('@language').its('request.body').should('eq', 'lang=be');
cy.findByTestId('language-switcher').should('not.be.visible');
cy.findByTestId('profile-index').should('contain', 'Беларуская');
@@ -7,11 +7,9 @@ describe('Profile — mfa', () => {
const totp = 'totp123';
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.server();
cy.route({
method: 'POST',
url: `/api/v1/accounts/${account.id}/two-factor-auth`,
response: { success: false, errors: { password: 'foo' } },
cy.intercept('POST', `/api/v1/accounts/${account.id}/two-factor-auth`, {
success: false,
errors: { password: 'foo' },
}).as('mfaSaved');
cy.visit('/');
@@ -38,7 +36,7 @@ describe('Profile — mfa', () => {
cy.get('[name=totp]').type(`${totp}{enter}`);
cy.wait('@mfaSaved')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -47,35 +45,29 @@ describe('Profile — mfa', () => {
}).toString(),
);
cy.route({
method: 'POST',
url: `/api/v1/accounts/${account.id}/two-factor-auth`,
response: { success: true },
}).as('mfaSaved');
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account.id}`,
response: {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: true, // fake enabled mfa
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: false,
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse,
});
cy.intercept('POST', `/api/v1/accounts/${account.id}/two-factor-auth`, { success: true }).as(
'mfaSaved',
);
cy.intercept('GET', `/api/v1/accounts/${account.id}`, {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: true, // fake enabled mfa
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: false,
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse);
confirmWithPassword(account.password);
cy.wait('@mfaSaved')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -93,30 +85,24 @@ describe('Profile — mfa', () => {
const totp = 'totp123';
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.server();
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account.id}`,
response: {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: true, // fake enabled mfa
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: false,
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse,
});
cy.route({
method: 'DELETE',
url: `/api/v1/accounts/${account.id}/two-factor-auth`,
response: { success: false, errors: { password: 'foo' } },
cy.intercept('GET', `/api/v1/accounts/${account.id}`, {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: true, // fake enabled mfa
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: false,
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse);
cy.intercept('DELETE', `/api/v1/accounts/${account.id}/two-factor-auth`, {
success: false,
errors: { password: 'foo' },
}).as('mfaSaved');
cy.visit('/');
@@ -131,7 +117,7 @@ describe('Profile — mfa', () => {
cy.get('[name=totp]').type(`${totp}{enter}`);
cy.wait('@mfaSaved')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -140,21 +126,16 @@ describe('Profile — mfa', () => {
}).toString(),
);
cy.route({
method: 'DELETE',
url: `/api/v1/accounts/${account.id}/two-factor-auth`,
response: { success: true },
}).as('mfaSaved');
cy.intercept('DELETE', `/api/v1/accounts/${account.id}/two-factor-auth`, { success: true }).as(
'mfaSaved',
);
// unmock accounts route
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account.id}`,
});
cy.intercept('GET', `/api/v1/accounts/${account.id}`, (req) => req.continue());
confirmWithPassword(account.password);
cy.wait('@mfaSaved')
.its('requestBody')
.its('request.body')
.should(
'eq',
new URLSearchParams({
@@ -0,0 +1,50 @@
import { UserResponse } from 'app/services/api/accounts';
describe('Profile — Restore account', () => {
it('should restore account', () => {
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.intercept('GET', `/api/v1/accounts/${account.id}`, {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'FooBar',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: true, // force deleted state
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse);
cy.intercept('POST', `/api/v1/accounts/${account.id}/restore`, { success: true }).as('restoreAccount');
cy.visit('/');
cy.intercept('GET', `/api/v1/accounts/${account.id}`, {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: false, // force deleted state
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse);
cy.findByTestId('deletedAccount').contains('Restore account').click();
cy.wait('@restoreAccount');
cy.location('pathname').should('eq', '/');
cy.findByTestId('profile-index').should('contain', account.username);
});
});
});
@@ -1,75 +0,0 @@
import { openSectionByName, confirmWithPassword } from './utils';
import { UserResponse } from 'app/services/api/accounts';
describe('Profile — Delete account', () => {
it('should delete account', () => {
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.server();
cy.route({
method: 'DELETE',
url: `/api/v1/accounts/${account.id}`,
}).as('deleteAccount');
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account.id}`,
});
cy.visit('/');
openSectionByName('Account deletion');
cy.location('pathname').should('eq', '/profile/delete');
cy.get('[type=submit]').click();
cy.wait('@deleteAccount')
.its('requestBody')
.should(
'eq',
new URLSearchParams({
password: '',
}).toString(),
);
cy.route({
method: 'DELETE',
url: `/api/v1/accounts/${account.id}`,
response: { success: true },
}).as('deleteAccount');
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account.id}`,
response: {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: true, // mock deleted state since the delete will not perform the real request
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse,
});
confirmWithPassword(account.password);
cy.wait('@deleteAccount')
.its('requestBody')
.should(
'eq',
new URLSearchParams({
password: account.password,
}).toString(),
);
cy.location('pathname').should('eq', '/');
cy.findByTestId('deletedAccount').should('contain', 'Account is deleted');
});
});
});
@@ -1,63 +0,0 @@
import { UserResponse } from 'app/services/api/accounts';
describe('Profile — Restore account', () => {
it('should restore account', () => {
cy.login({ accounts: ['default'] }).then(({ accounts: [account] }) => {
cy.server();
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account.id}`,
response: {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'FooBar',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: true, // force deleted state
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse,
});
cy.route({
method: 'POST',
url: `/api/v1/accounts/${account.id}/restore`,
response: { success: true },
}).as('restoreAccount');
cy.visit('/');
cy.route({
method: 'GET',
url: `/api/v1/accounts/${account.id}`,
response: {
id: 7,
uuid: '522e8c19-89d8-4a6d-a2ec-72ebb58c2dbe',
username: 'SleepWalker',
isOtpEnabled: false,
registeredAt: 1475568334,
lang: 'en',
elyProfileLink: 'http://ely.by/u7',
email: 'danilenkos@auroraglobal.com',
isActive: true,
isDeleted: false, // force deleted state
passwordChangedAt: 1476075696,
hasMojangUsernameCollision: true,
shouldAcceptRules: false,
} as UserResponse,
});
cy.findByTestId('deletedAccount').contains('Restore account').click();
cy.wait('@restoreAccount');
cy.location('pathname').should('eq', '/');
cy.findByTestId('profile-index').should('contain', account.username);
});
});
});
-79
View File
@@ -1,79 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/* eslint-env node */
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
// export default (on, config) => {
// // `on` is used to hook into various events Cypress emits
// // `config` is the resolved Cypress config
// };
const wp = require('@cypress/webpack-preprocessor');
// for some reason loader can not locate babel.config. So we load it manually
const config = require('../../../babel.config');
const babelEnvName = 'browser-development';
module.exports = (on) => {
const options = {
webpackOptions: {
mode: 'development',
// webpack will transpile TS and JS files
resolve: {
extensions: ['.ts', '.js', '.json'],
},
module: {
rules: [
{
test: /\.[tj]s$/,
exclude: [/node_modules/],
use: [
{
loader: 'babel-loader',
options: {
envName: babelEnvName,
cacheDirectory: true,
// We don't have the webpack's API object, so just provide necessary methods
...config({
env(value) {
// see @babel/core/lib/config/helpers/config-api.js
switch (typeof value) {
case 'string':
return value === babelEnvName;
case 'function':
return value(babelEnvName);
case 'undefined':
return babelEnvName;
default:
if (Array.isArray(value)) {
throw new Error('Unimplemented env() argument');
}
throw new Error('Invalid env() argument');
}
},
cache: {
using() {},
},
}),
},
},
],
},
],
},
},
};
on('file:preprocessor', wp(options));
};
@@ -1,34 +1,44 @@
import '@testing-library/cypress/add-commands';
import { account1, account2 } from '../fixtures/accounts';
import { account1, account2 } from '../fixtures/accounts.json';
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
// During a panel transition PanelTransition (app/components/auth/PanelTransition.tsx) keeps both the leaving
// and the entering panels in the DOM. Each panel is marked with `data-e2e-panel-active`, so the queries below
// ignore everything inside an inactive panel and tests don't need to know about the transitions at all.
const panelAwareQueries: Array<keyof Cypress.Chainable> = [
'get',
'contains',
'findByLabelText',
'findAllByLabelText',
'findByPlaceholderText',
'findAllByPlaceholderText',
'findByText',
'findAllByText',
'findByDisplayValue',
'findAllByDisplayValue',
'findByAltText',
'findAllByAltText',
'findByTitle',
'findAllByTitle',
'findByRole',
'findAllByRole',
'findByTestId',
'findAllByTestId',
];
const accountsMap = {
panelAwareQueries.forEach((command) => {
Cypress.Commands.overwriteQuery(command, function (originalFn, ...args) {
const getElements = originalFn.apply(this, args);
return (subject: unknown) => {
return getElements(subject).filter(
(_index: number, el: HTMLElement) => !el.closest('[data-e2e-panel-active="false"]'),
);
};
});
});
const accountsMap: Record<AccountAlias, typeof account1> = {
default: account1,
default2: account2,
};
@@ -36,14 +46,10 @@ const accountsMap = {
Cypress.Commands.add('login', async ({ accounts, updateState = true, rawApiResp = false }) => {
const accountsData = await Promise.all(
accounts.map(async (account) => {
let credentials;
const credentials = accountsMap[account];
if (account) {
credentials = accountsMap[account];
if (!credentials) {
throw new Error(`Unknown account name: ${account}`);
}
if (!credentials) {
throw new Error(`Unknown account name: ${account}`);
}
const resp = await fetch('/api/authentication/login', {
@@ -82,7 +88,7 @@ Cypress.Commands.add('login', async ({ accounts, updateState = true, rawApiResp
return { accounts: accountsData };
});
function createState(accounts) {
function createState(accounts: Array<{ id: number }>) {
return {
accounts: {
available: accounts,
+28
View File
@@ -0,0 +1,28 @@
import './commands';
Cypress.on('window:before:load', (win) => {
// The browser extends the system's language. Not everyone so cool to use English on their workstation,
// so we must force browser's language to be English to let tests, based on buttons labels, work
Object.defineProperty(win.navigator, 'languages', {
get() {
return ['en-US', 'en'];
},
});
});
beforeEach(() => {
// Headless Chromium-based browsers deny the clipboard access by default, which makes the app's "copy" buttons
// reject with an unhandled error. Grant the permission like a real user would do it
if (Cypress.browser.family === 'chromium' && Cypress.browser.name !== 'electron') {
cy.wrap(
Cypress.automation('remote:debugger:protocol', {
command: 'Browser.grantPermissions',
params: {
permissions: ['clipboardReadWrite', 'clipboardSanitizedWrite'],
origin: Cypress.config('baseUrl'),
},
}),
{ log: false },
);
}
});
-13
View File
@@ -1,13 +0,0 @@
import './commands';
Cypress.on('window:before:load', (win) => {
// Remove fetch to enable correct api mocking with the cypress xhr mocks
win.fetch = null;
// The browser extends the system's language. Not everyone so cool to use English on their workstation,
// so we must force browser's language to be English to let tests, based on buttons labels, work
Object.defineProperty(win.navigator, 'languages', {
get() {
return ['en-US', 'en'];
},
});
});
+3 -5
View File
@@ -4,7 +4,7 @@
"description": "account.ely.by e2e test suit",
"main": "index.js",
"scripts": {
"test": "cypress open",
"test": "cypress open --e2e --browser chromium",
"test:ci": "cypress run"
},
"keywords": [],
@@ -12,9 +12,7 @@
"license": "UNLICENSED",
"private": true,
"dependencies": {
"@cypress/webpack-preprocessor": "^5.4.1",
"@testing-library/cypress": "^6.0.0",
"@types/testing-library__cypress": "^5.0.5",
"cypress": "^4.6.0"
"@testing-library/cypress": "^10.0.3",
"cypress": "^16.1.1"
}
}
+10 -12
View File
@@ -1,16 +1,14 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"strict": true,
"baseUrl": "../node_modules",
"module": "esnext",
"target": "es5",
"esModuleInterop": true,
"noEmit": true,
"moduleResolution": "node",
"lib": ["es6", "dom"],
"types": ["cypress", "@types/testing-library__cypress"],
"resolveJsonModule": true,
"noImplicitAny": false
// cypress ships its own types (not under @types/), and root doesn't declare an
// explicit "types" array, so these need to be listed here to get `cy`/`Cypress`
// globals without an explicit reference/import in every spec file.
// "node" is for the Cypress config and the app's sources, reachable through the `app/...` type imports
"types": ["cypress", "@testing-library/cypress", "node"],
// fixtures are imported as `*.json`; root disables this because it conflicts with
// `*.intl.json` overrides in the app, which doesn't apply to the test suite
"resolveJsonModule": true
},
"include": ["cypress/**/*"]
"include": ["../@types", "cypress/**/*", "cypress.config.ts"]
}
+83 -775
View File
File diff suppressed because it is too large. Load diff