UNPKG

@commercetools-frontend/cypress

Version:
537 lines (505 loc) • 24.7 kB
'use strict'; var _Object$keys = require('@babel/runtime-corejs3/core-js-stable/object/keys'); var _Object$getOwnPropertySymbols = require('@babel/runtime-corejs3/core-js-stable/object/get-own-property-symbols'); var _filterInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/filter'); var _Object$getOwnPropertyDescriptor = require('@babel/runtime-corejs3/core-js-stable/object/get-own-property-descriptor'); var _forEachInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/for-each'); var _Object$getOwnPropertyDescriptors = require('@babel/runtime-corejs3/core-js-stable/object/get-own-property-descriptors'); var _Object$defineProperties = require('@babel/runtime-corejs3/core-js-stable/object/define-properties'); var _Object$defineProperty = require('@babel/runtime-corejs3/core-js-stable/object/define-property'); var _defineProperty = require('@babel/runtime-corejs3/helpers/defineProperty'); var _findInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/find'); var constants$1 = require('@commercetools-frontend/constants'); var _JSON$stringify = require('@babel/runtime-corejs3/core-js-stable/json/stringify'); var _URL = require('@babel/runtime-corejs3/core-js-stable/url'); var semver = require('semver'); var uuid = require('uuid'); var ssr = require('@commercetools-frontend/application-shell/ssr'); var constants = require('../../dist/constants-2f1475a6.cjs.prod.js'); var _slicedToArray = require('@babel/runtime-corejs3/helpers/slicedToArray'); var _mapInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/map'); var _Array$from = require('@babel/runtime-corejs3/core-js-stable/array/from'); var _entriesInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/entries'); var _reduceRightInstanceProperty = require('@babel/runtime-corejs3/core-js-stable/instance/reduce-right'); function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; } var _Object$keys__default = /*#__PURE__*/_interopDefault(_Object$keys); var _Object$getOwnPropertySymbols__default = /*#__PURE__*/_interopDefault(_Object$getOwnPropertySymbols); var _filterInstanceProperty__default = /*#__PURE__*/_interopDefault(_filterInstanceProperty); var _Object$getOwnPropertyDescriptor__default = /*#__PURE__*/_interopDefault(_Object$getOwnPropertyDescriptor); var _forEachInstanceProperty__default = /*#__PURE__*/_interopDefault(_forEachInstanceProperty); var _Object$getOwnPropertyDescriptors__default = /*#__PURE__*/_interopDefault(_Object$getOwnPropertyDescriptors); var _Object$defineProperties__default = /*#__PURE__*/_interopDefault(_Object$defineProperties); var _Object$defineProperty__default = /*#__PURE__*/_interopDefault(_Object$defineProperty); var _findInstanceProperty__default = /*#__PURE__*/_interopDefault(_findInstanceProperty); var _JSON$stringify__default = /*#__PURE__*/_interopDefault(_JSON$stringify); var _URL__default = /*#__PURE__*/_interopDefault(_URL); var semver__default = /*#__PURE__*/_interopDefault(semver); var _mapInstanceProperty__default = /*#__PURE__*/_interopDefault(_mapInstanceProperty); var _Array$from__default = /*#__PURE__*/_interopDefault(_Array$from); var _entriesInstanceProperty__default = /*#__PURE__*/_interopDefault(_entriesInstanceProperty); var _reduceRightInstanceProperty__default = /*#__PURE__*/_interopDefault(_reduceRightInstanceProperty); function ownKeys$1(e, r) { var t = _Object$keys__default["default"](e); if (_Object$getOwnPropertySymbols__default["default"]) { var o = _Object$getOwnPropertySymbols__default["default"](e); r && (o = _filterInstanceProperty__default["default"](o).call(o, function (r) { return _Object$getOwnPropertyDescriptor__default["default"](e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread$1(e) { for (var r = 1; r < arguments.length; r++) { var _context, _context2; var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? _forEachInstanceProperty__default["default"](_context = ownKeys$1(Object(t), !0)).call(_context, function (r) { _defineProperty(e, r, t[r]); }) : _Object$getOwnPropertyDescriptors__default["default"] ? _Object$defineProperties__default["default"](e, _Object$getOwnPropertyDescriptors__default["default"](t)) : _forEachInstanceProperty__default["default"](_context2 = ownKeys$1(Object(t))).call(_context2, function (r) { _Object$defineProperty__default["default"](e, r, _Object$getOwnPropertyDescriptor__default["default"](t, r)); }); } return e; } // eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any // Alias for backwards compatibility function isFeatureSupported(expectedVersion) { return semver__default["default"].gte(Cypress.version, expectedVersion); } function loginByForm(commandOptions) { if (isLocalhost()) { throw new Error(`At the moment, the "loginByForm" command only works when testing a Merchant Center production URL. Using form login in an application running on localhost is not supported due to issues with "cy.origin".`); } const projectKey = commandOptions.projectKey ?? Cypress.env('PROJECT_KEY'); cy.task('customApplicationConfig', { entryPointUriPath: commandOptions.entryPointUriPath, dotfiles: commandOptions.dotfiles }, // Do not show log, as it may contain sensible information. { log: false }).then(appConfig => { let url = `/${projectKey}/${commandOptions.entryPointUriPath}`; if (commandOptions.entryPointUriPath === 'account') { url = `/${commandOptions.entryPointUriPath}`; } // Log loaded application config for debugging purposes. Cypress.log({ displayName: 'task', name: 'customApplicationConfig', message: appConfig }); const userCredentials = commandOptions.login ?? { email: Cypress.env('LOGIN_EMAIL') || Cypress.env('LOGIN_USER'), password: Cypress.env('LOGIN_PASSWORD') }; const sessionKey = ['loginByForm', userCredentials.email, commandOptions.entryPointUriPath]; // const mcUrl = new URL(appConfig.mcApiUrl); // const mcFrontendHostname = mcUrl.hostname.replace('mc-api', 'mc'); function authCallback() { cy.visit(url, { onBeforeLoad: commandOptions.onBeforeLoad }); // NOTE: using `cy.origin` is currently disabled as it does not seem to properly work. // Interestingly, starting an application locally using Vite works, however not when using Webpack. // For now we keep it disabled until we find a solution. // // https://cypress.io/blog/2022/04/25/cypress-9-6-0-easily-test-multi-domain-workflows-with-cy-origin/ // cy.origin( // mcFrontendHostname, // { args: userCredentials }, // fillLoginForm // ); fillLoginForm(userCredentials); // Wait for the route to be loaded so that the session can be saved. cy.url().should('include', url); } // For backwards compatibility. if (isFeatureSupported('12.0.0') || Cypress.config('experimentalSessionAndOrigin')) { // https://www.cypress.io/blog/2021/08/04/authenticate-faster-in-tests-cy-session-command/ cy.session(sessionKey, authCallback, isFeatureSupported('10.9.0') ? { cacheAcrossSpecs: typeof commandOptions.disableCacheAcrossSpecs === 'boolean' ? !commandOptions.disableCacheAcrossSpecs : true } : undefined); } else { cy.log(`We recommend to use "cy.session" to reduce the time to log in between tests. Make sure to have at least Cypress v12 or enable it via "experimentalSessionAndOrigin" for older Cypress versions.`); authCallback(); } if (commandOptions.initialRoute) { cy.visit(`${Cypress.config('baseUrl')}${commandOptions.initialRoute}`); cy.url().should('include', commandOptions.initialRoute); } }); } const isCustomView = commandOptions => commandOptions.entryPointUriPath === constants$1.CUSTOM_VIEW_HOST_ENTRY_POINT_URI_PATH; function loginByOidc(commandOptions) { const isCustomViewConfigCommand = isCustomView(commandOptions); if (!isLocalhost()) { throw new Error(`The "loginByOidc" command only works when testing a Custom ${isCustomViewConfigCommand ? 'View' : 'Application'} running on localhost.`); } const sessionNonce = uuid.v4(); let projectKey = undefined; if (commandOptions.entryPointUriPath !== 'account') { projectKey = commandOptions.projectKey ?? Cypress.env('PROJECT_KEY'); } const customEntityConfigCommand = isCustomViewConfigCommand ? 'customViewConfig' : 'customApplicationConfig'; const packageName = commandOptions.packageName ?? Cypress.env('PACKAGE_NAME'); if (isCustomViewConfigCommand && !packageName) { throw new Error(`Missing required option "packageName" when using the "loginToMerchantCenterForCustomView" command.`); } cy.task(customEntityConfigCommand, _objectSpread$1({ entryPointUriPath: commandOptions.entryPointUriPath, dotfiles: commandOptions.dotfiles }, isCustomViewConfigCommand ? { packageName } : {}), // Do not show log, as it may contain sensible information. { log: false }).then(appConfig => { // Log loaded application config for debugging purposes. Cypress.log({ displayName: 'task', name: customEntityConfigCommand, message: appConfig }); const applicationId = appConfig.applicationId; const sessionScope = ssr.buildOidcScope({ projectKey, oAuthScopes: appConfig.__DEVELOPMENT__?.oidc?.oAuthScopes, additionalOAuthScopes: appConfig.__DEVELOPMENT__?.oidc?.additionalOAuthScopes, teamId: appConfig.__DEVELOPMENT__?.oidc?.teamId, applicationId: appConfig.__DEVELOPMENT__?.oidc?.applicationId }); const userCredentials = commandOptions.login ?? { email: Cypress.env('LOGIN_EMAIL') || Cypress.env('LOGIN_USER'), password: Cypress.env('LOGIN_PASSWORD') }; // Perform the login using the API, then store some required values into the browser storage // and redirect to the auth callback route. const requestOptions = { method: 'POST', url: `${appConfig.mcApiUrl}/tokens`, body: _objectSpread$1(_objectSpread$1({}, userCredentials), {}, { client_id: applicationId, response_type: constants.OIDC_RESPONSE_TYPES.ID_TOKEN, scope: sessionScope, state: sessionNonce, nonce: sessionNonce }), followRedirect: false }; cy.request(requestOptions).then(res => { const sessionKey = ['loginByOidc', userCredentials.email, commandOptions.entryPointUriPath]; function authCallback() { cy.visit(res.body.redirectTo, { onBeforeLoad(win) { if (projectKey) { win.localStorage.setItem(constants.STORAGE_KEYS.ACTIVE_PROJECT_KEY, projectKey); } win.sessionStorage.setItem(`${constants.STORAGE_KEYS.NONCE}_${sessionNonce}`, _JSON$stringify__default["default"]({ applicationId, query: {} })); win.sessionStorage.setItem(constants.STORAGE_KEYS.SESSION_SCOPE, sessionScope); if (commandOptions.onBeforeLoad) { commandOptions.onBeforeLoad(win); } } }); // Wait for the application to be loaded so that the session can be saved. cy.get('#app-loader').should('not.exist'); } // For backwards compatibility. if (isFeatureSupported('12.0.0') || Cypress.config('experimentalSessionAndOrigin')) { // https://www.cypress.io/blog/2021/08/04/authenticate-faster-in-tests-cy-session-command/ cy.session(sessionKey, authCallback, isFeatureSupported('10.9.0') ? { cacheAcrossSpecs: typeof commandOptions.disableCacheAcrossSpecs === 'boolean' ? !commandOptions.disableCacheAcrossSpecs : true } : undefined); } else { cy.log(`We recommend to use "cy.session" to reduce the time to log in between tests. Make sure to have at least Cypress v12 or enable it via "experimentalSessionAndOrigin" for older Cypress versions.`); authCallback(); } if (commandOptions.initialRoute) { cy.visit(`${Cypress.config('baseUrl')}${commandOptions.initialRoute}`); cy.url().should('include', commandOptions.initialRoute); } }); }); } /* Utilities */ const maxLoginAttempts = Cypress.config('maxLoginAttempts') ?? 3; function fillLoginForm(userCredentials) { // Intercept the login request so we can retry it if we receive a TOO_MANY_REQUESTS status code cy.intercept('POST', '**/tokens').as('loginRequest'); function getRandomDelayInSeconds() { const minSeconds = 0.5; const maxSeconds = 1.5; return (Math.random() * (maxSeconds - minSeconds) + minSeconds) * 1000; } function attemptLogin(attemptsLeft) { if (attemptsLeft <= 0) { throw new Error(`All login attempts exhausted. Please check your credentials.`); } cy.log(`Attempts left: ${attemptsLeft}`); // eslint-disable-next-line cypress/unsafe-to-chain-command cy.get('input[name=email]').clear().type(userCredentials.email); // eslint-disable-next-line cypress/unsafe-to-chain-command cy.get('input[name=password]').clear().type(userCredentials.password, { log: false }); cy.get('button').contains('Sign in').click(); // eslint-disable-next-line @typescript-eslint/no-explicit-any cy.wait('@loginRequest').then(interception => { const statusCode = interception.response.statusCode; cy.log('Login request status code:', statusCode); if (statusCode === constants.HTTP_STATUS_CODES.TOO_MANY_REQUESTS) { // We wait for something between 0.5 and 1.5 seconds before retrying cy.wait(getRandomDelayInSeconds()); attemptLogin(attemptsLeft - 1); } else { cy.log('Login successful'); } }); } attemptLogin(maxLoginAttempts); } function isLocalhost() { const baseUrl = new _URL__default["default"](Cypress.config('baseUrl')); return baseUrl.hostname === 'localhost'; } /** * NOTE: the `realHover` command is originally being implemented in `cypress-real-events` package. * https://github.com/dmtrKovalenko/cypress-real-events/blob/develop/src/commands/realHover.ts * * However, due to known issues with conflicting types between Cypress and Jest, importing the `cypress-real-events` * package here will cause such issues with TypeScript as our `@commercetools-frontend/cypress` package * is checked and built together with all other packages and not in isolation. * See https://docs.cypress.io/guides/tooling/typescript-support#Clashing-types-with-Jest. * * Therefore, we are porting here the implementation of `realHover` to avoid importing it from the * original package `cypress-real-events`. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any async function fireCdpCommand(command, params) { return Cypress.automation('remote:debugger:protocol', { command, params }).catch(error => { throw new Error(`Failed request to chrome devtools protocol. This can happen if cypress lost connection to the browser or the command itself is not valid. Original cypress error: ${error}`); }); } function getPositionedCoordinates(x0, y0, width, height, position, frameScale) { if (typeof position === 'object' && position !== null) { const x = position.x, y = position.y; // scale the position coordinates according to the viewport scale return [x0 + x * frameScale, y0 + y * frameScale]; } switch (position) { case 'topLeft': return [x0, y0]; case 'top': return [x0 + width / 2, y0]; case 'topRight': return [x0 + width - 1, y0]; case 'left': return [x0, y0 + height / 2]; case 'right': return [x0 + width - 1, y0 + height / 2]; case 'bottomLeft': return [x0, y0 + height - 1]; case 'bottom': return [x0 + width / 2, y0 + height - 1]; case 'bottomRight': return [x0 + width - 1, y0 + height - 1]; // center by default default: return [x0 + width / 2, y0 + height / 2]; } } /** * Scrolls the given htmlElement into view on the page. * The position the element is scrolled to can be customized with scrollBehavior. */ function scrollIntoView(htmlElement) { let scrollBehavior = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'center'; let block; if (scrollBehavior === 'top') { block = 'start'; } else if (scrollBehavior === 'bottom') { block = 'end'; } else { block = scrollBehavior; } htmlElement.scrollIntoView({ block }); } // for cross origin domains .frameElement returns null so query using parentWindow // but when running using --disable-web-security it will return the frame element function getFrameElement(currentWindow) { var _context, _context2; if (currentWindow.frameElement) { // accessible for same-origin iframes // or when running with --disable-web-security return currentWindow.frameElement; } // fallback to querying using the parent window, mainly to grab the AUT iframe const iframeElements = currentWindow.parent.document.querySelectorAll('iframe'); return _findInstanceProperty__default["default"](_context = _mapInstanceProperty__default["default"](_context2 = _Array$from__default["default"](_entriesInstanceProperty__default["default"](iframeElements).call(iframeElements))).call(_context2, _ref => { let _ref2 = _slicedToArray(_ref, 2), element = _ref2[1]; return element; })).call(_context, element => element.contentWindow === currentWindow); } function getIframesPositionShift(element) { let currentWindow = element.ownerDocument.defaultView; const noPositionShift = { frameScale: 1, frameX: 0, frameY: 0 }; if (!currentWindow) { return noPositionShift; } // eslint-disable-next-line prefer-const const iframes = []; while (currentWindow !== window.top) { const element = getFrameElement(currentWindow); if (element) { iframes.push(element); } currentWindow = currentWindow.parent; } return _reduceRightInstanceProperty__default["default"](iframes).call(iframes, (_ref3, frame) => { let frameX = _ref3.frameX, frameY = _ref3.frameY, frameScale = _ref3.frameScale; const _frame$getBoundingCli = frame.getBoundingClientRect(), x = _frame$getBoundingCli.x, y = _frame$getBoundingCli.y, width = _frame$getBoundingCli.width; return { frameX: frameX + x * frameScale, frameY: frameY + y * frameScale, frameScale: frameScale * (width / frame.offsetWidth) }; }, noPositionShift); } /** * Returns the coordinates and size of a given Element, relative to the Cypress app <iframe>. * Accounts for any scaling on the iframes. */ function getElementPositionXY(htmlElement) { const _htmlElement$getBound = htmlElement.getBoundingClientRect(), elementX = _htmlElement$getBound.x, elementY = _htmlElement$getBound.y, width = _htmlElement$getBound.width, height = _htmlElement$getBound.height; const _getIframesPositionSh = getIframesPositionShift(htmlElement), frameScale = _getIframesPositionSh.frameScale, frameX = _getIframesPositionSh.frameX, frameY = _getIframesPositionSh.frameY; return { x: frameX + elementX * frameScale, y: frameY + elementY * frameScale, width: width * frameScale, height: height * frameScale, frameScale: frameScale }; } function getCypressElementCoordinates( // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-explicit-any jqueryEl, position, scrollBehavior) { const htmlElement = jqueryEl.get(0); const cypressAppFrame = window.parent.document.querySelector('iframe'); if (!cypressAppFrame) { throw new Error('Can not find cypress application iframe, it looks like critical issue. Please rise an issue on GitHub.'); } const effectiveScrollBehavior = scrollBehavior ?? Cypress.config('scrollBehavior') ?? 'center'; if (effectiveScrollBehavior && typeof effectiveScrollBehavior !== 'object') { scrollIntoView(htmlElement, effectiveScrollBehavior); } const _getElementPositionXY = getElementPositionXY(htmlElement), x = _getElementPositionXY.x, y = _getElementPositionXY.y, width = _getElementPositionXY.width, height = _getElementPositionXY.height, frameScale = _getElementPositionXY.frameScale; const _getPositionedCoordin = getPositionedCoordinates(x, y, width, height, position ?? 'center', frameScale), _getPositionedCoordin2 = _slicedToArray(_getPositionedCoordin, 2), posX = _getPositionedCoordin2[0], posY = _getPositionedCoordin2[1]; return { x: posX, y: posY, frameScale: frameScale }; } const keyToModifierBitMap = { Alt: 1, Control: 2, Meta: 4, Shift: 8 }; async function realHover( // @ts-ignore // eslint-disable-next-line @typescript-eslint/no-explicit-any subject) { let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; const _getCypressElementCoo = getCypressElementCoordinates(subject, options.position, options.scrollBehavior), x = _getCypressElementCoo.x, y = _getCypressElementCoo.y; const log = Cypress.log({ $el: subject, name: 'realHover', consoleProps: () => ({ 'Applied To': subject.get(0), 'Absolute Coordinates': { x, y } }) }); await fireCdpCommand('Input.dispatchMouseEvent', { x, y, type: 'mouseMoved', button: 'none', pointerType: options.pointer ?? 'mouse', modifiers: options.shiftKey ? keyToModifierBitMap.Shift : 0 }); log.snapshot().end(); return subject; } function ownKeys(e, r) { var t = _Object$keys__default["default"](e); if (_Object$getOwnPropertySymbols__default["default"]) { var o = _Object$getOwnPropertySymbols__default["default"](e); r && (o = _filterInstanceProperty__default["default"](o).call(o, function (r) { return _Object$getOwnPropertyDescriptor__default["default"](e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var _context2, _context3; var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? _forEachInstanceProperty__default["default"](_context2 = ownKeys(Object(t), !0)).call(_context2, function (r) { _defineProperty(e, r, t[r]); }) : _Object$getOwnPropertyDescriptors__default["default"] ? _Object$defineProperties__default["default"](e, _Object$getOwnPropertyDescriptors__default["default"](t)) : _forEachInstanceProperty__default["default"](_context3 = ownKeys(Object(t))).call(_context3, function (r) { _Object$defineProperty__default["default"](e, r, _Object$getOwnPropertyDescriptor__default["default"](t, r)); }); } return e; } // eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any Cypress.Commands.add('loginToMerchantCenter', commandOptions => { Cypress.log({ name: 'loginToMerchantCenter' }); if (isLocalhost()) { loginByOidc(commandOptions); } else { loginByForm(commandOptions); } }); Cypress.Commands.add('loginToMerchantCenterForCustomView', commandOptions => { Cypress.log({ name: 'loginToMerchantCenterForCustomView' }); const projectKey = Cypress.env('PROJECT_KEY'); loginByOidc(_objectSpread(_objectSpread({}, commandOptions), {}, { entryPointUriPath: constants$1.CUSTOM_VIEW_HOST_ENTRY_POINT_URI_PATH, initialRoute: `/${projectKey}/${constants$1.CUSTOM_VIEW_HOST_ENTRY_POINT_URI_PATH}` })); }); Cypress.Commands.add('loginByOidc', commandOptions => { Cypress.log({ name: 'loginByOidc' }); cy.log('We recommend not to use the command "cy.loginByOidc" directly. Instead, use the more generic "cy.loginToMerchantCenter" command as it automatically detects which login mechanism to use.'); loginByOidc(commandOptions); }); Cypress.Commands.add('hover', { prevSubject: true }, realHover); Cypress.Commands.add('showNavigationSubmenuItems', menuItemTextMatcher => { cy.findByTestId('left-navigation').findByText(menuItemTextMatcher).parents('[role="menuitem"]').first() // Refers to the custom command "hover" .hover(); }); // https://github.com/cypress-io/cypress/issues/136#issuecomment-342391119 Cypress.Commands.add('getIframeBody', { prevSubject: 'element' }, // eslint-disable-next-line @typescript-eslint/no-explicit-any $iframe => { // eslint-disable-next-line @typescript-eslint/no-explicit-any return new Cypress.Promise(resolve => { var _context; resolve(_findInstanceProperty__default["default"](_context = $iframe.contents()).call(_context, 'body')); }); });