@commercetools-frontend/cypress
Version:
Cypress commands and utilities for Custom Applications
537 lines (505 loc) • 24.7 kB
JavaScript
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'));
});
});
;