[MM-47303] Migrate /enterprise/extend_session/not_extended_when_disabled e2e tests to TypeScript (#28465)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
b4f337f191
Коммит
2aaa38b368
@@ -19,7 +19,7 @@ export function verifyExtendedSession(testUser, sessionLengthInDays, channelUrl)
|
|||||||
|
|
||||||
// # Post a message to a channel
|
// # Post a message to a channel
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
cy.postMessage(now);
|
cy.postMessage(`${now}`);
|
||||||
|
|
||||||
// # Update user session which is to expire 20 sec from now
|
// # Update user session which is to expire 20 sec from now
|
||||||
const soonToExpire = getExpirationFromNow(thirtySeconds);
|
const soonToExpire = getExpirationFromNow(thirtySeconds);
|
||||||
@@ -53,7 +53,7 @@ export function verifyExtendedSession(testUser, sessionLengthInDays, channelUrl)
|
|||||||
|
|
||||||
// # Post multiple times to check that the session continues and doesn't redirect to login page
|
// # Post multiple times to check that the session continues and doesn't redirect to login page
|
||||||
Cypress._.times(20, (i) => {
|
Cypress._.times(20, (i) => {
|
||||||
cy.postMessage(i);
|
cy.postMessage(`${i}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
// Group: @channels @enterprise @not_cloud @extend_session
|
// Group: @channels @enterprise @not_cloud @extend_session
|
||||||
|
|
||||||
|
import {UserProfile} from '@mattermost/types/users';
|
||||||
import {verifyExtendedSession, verifyNotExtendedSession} from './helpers';
|
import {verifyExtendedSession, verifyNotExtendedSession} from './helpers';
|
||||||
|
|
||||||
describe('Extended Session Length', () => {
|
describe('Extended Session Length', () => {
|
||||||
@@ -16,10 +17,11 @@ describe('Extended Session Length', () => {
|
|||||||
const setting = {
|
const setting = {
|
||||||
ServiceSettings: {
|
ServiceSettings: {
|
||||||
SessionLengthWebInHours: sessionLengthInHours,
|
SessionLengthWebInHours: sessionLengthInHours,
|
||||||
|
ExtendSessionLengthWithActivity: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let emailUser;
|
let emailUser: UserProfile;
|
||||||
let offTopicUrl;
|
let offTopicUrl: string;
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.shouldNotRunOnCloudEdition();
|
cy.shouldNotRunOnCloudEdition();
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
// Group: @channels @enterprise @not_cloud @extend_session @ldap
|
// Group: @channels @enterprise @not_cloud @extend_session @ldap
|
||||||
|
|
||||||
|
import {UserProfile} from '@mattermost/types/users';
|
||||||
import ldapUsers from '../../../../../fixtures/ldap_users.json';
|
import ldapUsers from '../../../../../fixtures/ldap_users.json';
|
||||||
|
|
||||||
import {verifyExtendedSession, verifyNotExtendedSession} from './helpers';
|
import {verifyExtendedSession, verifyNotExtendedSession} from './helpers';
|
||||||
@@ -18,10 +19,11 @@ describe('Extended Session Length', () => {
|
|||||||
const setting = {
|
const setting = {
|
||||||
ServiceSettings: {
|
ServiceSettings: {
|
||||||
SessionLengthWebInHours: sessionLengthInHours,
|
SessionLengthWebInHours: sessionLengthInHours,
|
||||||
|
ExtendSessionLengthWithActivity: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
let testLdapUser;
|
let testLdapUser: UserProfile;
|
||||||
let offTopicUrl;
|
let offTopicUrl: string;
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.shouldNotRunOnCloudEdition();
|
cy.shouldNotRunOnCloudEdition();
|
||||||
@@ -14,9 +14,11 @@
|
|||||||
|
|
||||||
// Group: @channels @enterprise @not_cloud @extend_session @ldap @saml @keycloak
|
// Group: @channels @enterprise @not_cloud @extend_session @ldap @saml @keycloak
|
||||||
|
|
||||||
|
import {UserProfile} from '@mattermost/types/users';
|
||||||
import {getKeycloakServerSettings} from '../../../../../utils/config';
|
import {getKeycloakServerSettings} from '../../../../../utils/config';
|
||||||
|
|
||||||
import {verifyExtendedSession, verifyNotExtendedSession} from './helpers';
|
import {verifyExtendedSession, verifyNotExtendedSession} from './helpers';
|
||||||
|
import {LdapUser} from 'tests/support/ldap_server_commands';
|
||||||
|
|
||||||
describe('Extended Session Length', () => {
|
describe('Extended Session Length', () => {
|
||||||
const sessionLengthInDays = 1;
|
const sessionLengthInDays = 1;
|
||||||
@@ -24,13 +26,14 @@ describe('Extended Session Length', () => {
|
|||||||
const sessionConfig = {
|
const sessionConfig = {
|
||||||
ServiceSettings: {
|
ServiceSettings: {
|
||||||
SessionLengthSSOInDays: sessionLengthInDays,
|
SessionLengthSSOInDays: sessionLengthInDays,
|
||||||
|
ExtendSessionLengthWithActivity: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let testTeamId;
|
let testTeamId: string;
|
||||||
let testSamlUser;
|
let testSamlUser: UserProfile;
|
||||||
let offTopicUrl;
|
let offTopicUrl: string;
|
||||||
let samlLdapUser;
|
let samlLdapUser: LdapUser;
|
||||||
|
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.shouldNotRunOnCloudEdition();
|
cy.shouldNotRunOnCloudEdition();
|
||||||
2
e2e-tests/cypress/tests/support/api/ldap.d.ts
поставляемый
2
e2e-tests/cypress/tests/support/api/ldap.d.ts
поставляемый
@@ -43,6 +43,6 @@ declare namespace Cypress {
|
|||||||
* @example
|
* @example
|
||||||
* cy.apiSyncLDAPUser();
|
* cy.apiSyncLDAPUser();
|
||||||
*/
|
*/
|
||||||
apiSyncLDAPUser(): Chainable<UserProfile>;
|
apiSyncLDAPUser({ldapUser = {}, bypassTutorial = true}): Chainable<UserProfile>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
function delayRequestToRoutes(routes = [], delay = 0) {
|
import {ChainableT} from 'tests/types';
|
||||||
|
|
||||||
|
function delayRequestToRoutes(routes: string[] = [], delay = 0) {
|
||||||
cy.on('window:before:load', (win) => addDelay(win, routes, delay));
|
cy.on('window:before:load', (win) => addDelay(win, routes, delay));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -9,7 +11,7 @@ Cypress.Commands.add('delayRequestToRoutes', delayRequestToRoutes);
|
|||||||
|
|
||||||
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
|
||||||
const addDelay = (win, routes, delay) => {
|
const addDelay = (win, routes: string[], delay: number) => {
|
||||||
const fetch = win.fetch;
|
const fetch = win.fetch;
|
||||||
cy.stub(win, 'fetch').callsFake((...args) => {
|
cy.stub(win, 'fetch').callsFake((...args) => {
|
||||||
for (let i = 0; i < routes.length; i++) {
|
for (let i = 0; i < routes.length; i++) {
|
||||||
@@ -54,7 +56,8 @@ const mockWebsocketsFn = (win) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
connect() {
|
connect() {
|
||||||
this.wrappedSocket = new RealWebSocket(...args);
|
const [param1, restOfParams] = args;
|
||||||
|
this.wrappedSocket = new RealWebSocket(param1, restOfParams);
|
||||||
this.wrappedSocket.onopen = this.onopen;
|
this.wrappedSocket.onopen = this.onopen;
|
||||||
this.wrappedSocket.onmessage = this.onmessage;
|
this.wrappedSocket.onmessage = this.onmessage;
|
||||||
this.wrappedSocket.onerror = this.onerror;
|
this.wrappedSocket.onerror = this.onerror;
|
||||||
@@ -70,7 +73,7 @@ declare global {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
namespace Cypress {
|
namespace Cypress {
|
||||||
interface Chainable {
|
interface Chainable {
|
||||||
delayRequestToRoutes: typeof delayRequestToRoutes;
|
delayRequestToRoutes(routes: string[], delay: number): ChainableT<void>;
|
||||||
mockWebsockets: typeof mockWebsockets;
|
mockWebsockets: typeof mockWebsockets;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {ChainableT} from 'tests/types';
|
import {ChainableT} from 'tests/types';
|
||||||
|
import {LdapUser} from './ldap_server_commands';
|
||||||
import * as TIMEOUTS from '../fixtures/timeouts';
|
import * as TIMEOUTS from '../fixtures/timeouts';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -36,8 +37,8 @@ function keycloakGetAccessTokenAPI(): ChainableT<string> {
|
|||||||
data: 'grant_type=password&username=mmuser&password=mostest&client_id=admin-cli',
|
data: 'grant_type=password&username=mmuser&password=mostest&client_id=admin-cli',
|
||||||
}).then((response: any) => {
|
}).then((response: any) => {
|
||||||
expect(response.status).to.equal(200);
|
expect(response.status).to.equal(200);
|
||||||
const token = response.data.access_token;
|
const token: string = response.data.access_token;
|
||||||
return cy.wrap(token as string);
|
return cy.wrap(token);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +53,7 @@ Cypress.Commands.add('keycloakGetAccessTokenAPI', keycloakGetAccessTokenAPI);
|
|||||||
* @example
|
* @example
|
||||||
* cy.keycloakCreateUserAPI('abcde', {firstName: 'test', lastName: 'test', email: 'test', username: 'test', enabled: true,});
|
* cy.keycloakCreateUserAPI('abcde', {firstName: 'test', lastName: 'test', email: 'test', username: 'test', enabled: true,});
|
||||||
*/
|
*/
|
||||||
function keycloakCreateUserAPI(accessToken: string, user: any = {}) {
|
function keycloakCreateUserAPI(accessToken: string, user: any = {}): ChainableT {
|
||||||
const profile = buildProfile(user);
|
const profile = buildProfile(user);
|
||||||
return cy.task('keycloakRequest', {
|
return cy.task('keycloakRequest', {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
@@ -152,7 +153,6 @@ function keycloakDeleteUserAPI(accessToken: string, userId: string): ChainableT
|
|||||||
expect(response.data).is.empty;
|
expect(response.data).is.empty;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Cypress.Commands.add('keycloakDeleteUserAPI', keycloakDeleteUserAPI);
|
Cypress.Commands.add('keycloakDeleteUserAPI', keycloakDeleteUserAPI);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -179,7 +179,6 @@ function keycloakUpdateUserAPI(accessToken: string, userId: string, data: any):
|
|||||||
expect(response.data).is.empty;
|
expect(response.data).is.empty;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Cypress.Commands.add('keycloakUpdateUserAPI', keycloakUpdateUserAPI);
|
Cypress.Commands.add('keycloakUpdateUserAPI', keycloakUpdateUserAPI);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -191,7 +190,7 @@ Cypress.Commands.add('keycloakUpdateUserAPI', keycloakUpdateUserAPI);
|
|||||||
* @example
|
* @example
|
||||||
* cy.keycloakDeleteSessionAPI('abcde', '12345');
|
* cy.keycloakDeleteSessionAPI('abcde', '12345');
|
||||||
*/
|
*/
|
||||||
function keycloakDeleteSessionAPI(accessToken: string, sessionId: string): ChainableT<any> {
|
function keycloakDeleteSessionAPI(accessToken: string, sessionId: string): ChainableT {
|
||||||
return cy.task('keycloakRequest', {
|
return cy.task('keycloakRequest', {
|
||||||
baseUrl,
|
baseUrl,
|
||||||
path: `sessions/${sessionId}`,
|
path: `sessions/${sessionId}`,
|
||||||
@@ -293,7 +292,7 @@ Cypress.Commands.add('keycloakResetUsers', keycloakResetUsers);
|
|||||||
* @example
|
* @example
|
||||||
* cy.keycloakCreateUser({firstName: 'test', lastName: 'test', email: 'test', username: 'test', enabled: true});
|
* cy.keycloakCreateUser({firstName: 'test', lastName: 'test', email: 'test', username: 'test', enabled: true});
|
||||||
*/
|
*/
|
||||||
function keycloakCreateUser(accessToken, user: any): ChainableT {
|
function keycloakCreateUser(accessToken: string, user: any): ChainableT {
|
||||||
return cy.keycloakCreateUserAPI(accessToken, user).then(() => {
|
return cy.keycloakCreateUserAPI(accessToken, user).then(() => {
|
||||||
cy.keycloakGetUserAPI(accessToken, user.email).then((newId) => {
|
cy.keycloakGetUserAPI(accessToken, user.email).then((newId) => {
|
||||||
cy.keycloakResetPasswordAPI(accessToken, newId, user.password).then(() => {
|
cy.keycloakResetPasswordAPI(accessToken, newId, user.password).then(() => {
|
||||||
@@ -304,9 +303,15 @@ function keycloakCreateUser(accessToken, user: any): ChainableT {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Cypress.Commands.add('keycloakCreateUser', keycloakCreateUser);
|
Cypress.Commands.add('keycloakCreateUser', keycloakCreateUser);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* keycloakCreateUsers is a command that creates keycloak users.
|
||||||
|
* @param {User[]} users - an array of users
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* cy.keycloakCreateUsers(users);
|
||||||
|
*/
|
||||||
function keycloakCreateUsers(users = []) {
|
function keycloakCreateUsers(users = []) {
|
||||||
return cy.keycloakGetAccessTokenAPI().then((accessToken) => {
|
return cy.keycloakGetAccessTokenAPI().then((accessToken) => {
|
||||||
return users.forEach((user) => {
|
return users.forEach((user) => {
|
||||||
@@ -317,7 +322,15 @@ function keycloakCreateUsers(users = []) {
|
|||||||
|
|
||||||
Cypress.Commands.add('keycloakCreateUsers', keycloakCreateUsers);
|
Cypress.Commands.add('keycloakCreateUsers', keycloakCreateUsers);
|
||||||
|
|
||||||
function keycloakUpdateUser(userEmail, data): ChainableT {
|
/**
|
||||||
|
* keycloakUpdateUser is a command that updates a keycloak user data.
|
||||||
|
* @param {string} userEmail - the user email
|
||||||
|
* @param {any} data - the user data to update
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* cy.keycloakUpdateUser('user@example.com', {firstName: 'test', lastName: 'test'});
|
||||||
|
*/
|
||||||
|
function keycloakUpdateUser(userEmail: string, data: any) {
|
||||||
return cy.keycloakGetAccessTokenAPI().then((accessToken) => {
|
return cy.keycloakGetAccessTokenAPI().then((accessToken) => {
|
||||||
return cy.keycloakGetUserAPI(accessToken, userEmail).then((userId) => {
|
return cy.keycloakGetUserAPI(accessToken, userEmail).then((userId) => {
|
||||||
return cy.keycloakUpdateUserAPI(accessToken, userId, data);
|
return cy.keycloakUpdateUserAPI(accessToken, userId, data);
|
||||||
@@ -375,7 +388,7 @@ Cypress.Commands.add('checkKeycloakLoginPage', checkKeycloakLoginPage);
|
|||||||
* @example
|
* @example
|
||||||
* cy.doKeycloakLogin();
|
* cy.doKeycloakLogin();
|
||||||
*/
|
*/
|
||||||
function doKeycloakLogin(user) {
|
function doKeycloakLogin(user: LdapUser) {
|
||||||
cy.apiLogout();
|
cy.apiLogout();
|
||||||
cy.visit('/login');
|
cy.visit('/login');
|
||||||
cy.findByText('SAML').click();
|
cy.findByText('SAML').click();
|
||||||
@@ -418,7 +431,7 @@ declare global {
|
|||||||
keycloakSuspendUser(userEmail: string): ChainableT<void>;
|
keycloakSuspendUser(userEmail: string): ChainableT<void>;
|
||||||
keycloakUnsuspendUser: typeof keycloakUnsuspendUser;
|
keycloakUnsuspendUser: typeof keycloakUnsuspendUser;
|
||||||
checkKeycloakLoginPage: typeof checkKeycloakLoginPage;
|
checkKeycloakLoginPage: typeof checkKeycloakLoginPage;
|
||||||
doKeycloakLogin(user): ChainableT<void>;
|
doKeycloakLogin(user: LdapUser): ChainableT<void>;
|
||||||
verifyKeycloakLoginFailed: typeof verifyKeycloakLoginFailed;
|
verifyKeycloakLoginFailed: typeof verifyKeycloakLoginFailed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,17 @@ import {getRandomId} from '../utils';
|
|||||||
|
|
||||||
const ldapTmpFolder = 'ldap_tmp';
|
const ldapTmpFolder = 'ldap_tmp';
|
||||||
|
|
||||||
|
export interface LdapUser {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
email: string;
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
ldapfirstname: string;
|
||||||
|
ldaplastname: string;
|
||||||
|
keycloakId: string;
|
||||||
|
}
|
||||||
|
|
||||||
function modifyLDAPUsers(filename: string) {
|
function modifyLDAPUsers(filename: string) {
|
||||||
cy.exec(`ldapmodify -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest -H ldap://${Cypress.env('ldapServer')}:${Cypress.env('ldapPort')} -f tests/fixtures/${filename} -c`, {failOnNonZeroExit: false});
|
cy.exec(`ldapmodify -x -D "cn=admin,dc=mm,dc=test,dc=com" -w mostest -H ldap://${Cypress.env('ldapServer')}:${Cypress.env('ldapPort')} -f tests/fixtures/${filename} -c`, {failOnNonZeroExit: false});
|
||||||
}
|
}
|
||||||
@@ -18,7 +29,7 @@ function resetLDAPUsers() {
|
|||||||
|
|
||||||
Cypress.Commands.add('resetLDAPUsers', resetLDAPUsers);
|
Cypress.Commands.add('resetLDAPUsers', resetLDAPUsers);
|
||||||
|
|
||||||
function createLDAPUser({prefix = 'ldap', user = undefined} = {}): ChainableT<LdapUser> {
|
function createLDAPUser({prefix = 'ldap', user = null} = {}): ChainableT<LdapUser> {
|
||||||
const ldapUser = user || generateLDAPUser(prefix);
|
const ldapUser = user || generateLDAPUser(prefix);
|
||||||
const data = generateContent(ldapUser);
|
const data = generateContent(ldapUser);
|
||||||
const filename = `new_user_${Date.now()}.ldif`;
|
const filename = `new_user_${Date.now()}.ldif`;
|
||||||
@@ -27,13 +38,13 @@ function createLDAPUser({prefix = 'ldap', user = undefined} = {}): ChainableT<Ld
|
|||||||
cy.task('writeToFile', ({filename, fixturesFolder: ldapTmpFolder, data}));
|
cy.task('writeToFile', ({filename, fixturesFolder: ldapTmpFolder, data}));
|
||||||
|
|
||||||
return cy.ldapAdd(filePath).then(() => {
|
return cy.ldapAdd(filePath).then(() => {
|
||||||
return cy.wrap(ldapUser as LdapUser);
|
return cy.wrap<LdapUser>(ldapUser);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Cypress.Commands.add('createLDAPUser', createLDAPUser);
|
Cypress.Commands.add('createLDAPUser', createLDAPUser);
|
||||||
|
|
||||||
function updateLDAPUser(user: LdapUser): ChainableT<LdapUser> {
|
function updateLDAPUser(user: Partial<LdapUser>) {
|
||||||
const data = generateContent(user, true);
|
const data = generateContent(user, true);
|
||||||
const filename = `update_user_${Date.now()}.ldif`;
|
const filename = `update_user_${Date.now()}.ldif`;
|
||||||
const filePath = `tests/fixtures/${ldapTmpFolder}/${filename}`;
|
const filePath = `tests/fixtures/${ldapTmpFolder}/${filename}`;
|
||||||
@@ -81,7 +92,7 @@ function getLDAPCredentials() {
|
|||||||
return {host, bindDn, password};
|
return {host, bindDn, password};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateLDAPUser(prefix = 'ldap') {
|
export function generateLDAPUser(prefix = 'ldap'): LdapUser {
|
||||||
const randomId = getRandomId();
|
const randomId = getRandomId();
|
||||||
const username = `${prefix}user${randomId}`;
|
const username = `${prefix}user${randomId}`;
|
||||||
|
|
||||||
@@ -97,7 +108,7 @@ export function generateLDAPUser(prefix = 'ldap') {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateContent(user: Partial<LdapUser>, isUpdate = false) {
|
function generateContent(user: Partial<LdapUser> = {}, isUpdate = false) {
|
||||||
let deleteContent = '';
|
let deleteContent = '';
|
||||||
if (isUpdate) {
|
if (isUpdate) {
|
||||||
deleteContent = `dn: uid=${user.username},ou=e2etest,dc=mm,dc=test,dc=com
|
deleteContent = `dn: uid=${user.username},ou=e2etest,dc=mm,dc=test,dc=com
|
||||||
@@ -124,17 +135,6 @@ userPassword: Password1
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LdapUser {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
email: string;
|
|
||||||
firstname: string;
|
|
||||||
lastname: string;
|
|
||||||
ldapfirstname: string;
|
|
||||||
ldaplastname: string;
|
|
||||||
keycloakId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
namespace Cypress {
|
namespace Cypress {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user