Skip to content

Commit

Permalink
Adds support for Nightwatch, WebdriverIO and Puppeteer (#3787)
Browse files Browse the repository at this point in the history
  • Loading branch information
walmazacn authored Jun 26, 2024
1 parent 5f4e776 commit 3654927
Show file tree
Hide file tree
Showing 4 changed files with 390 additions and 49 deletions.
160 changes: 155 additions & 5 deletions client-frameworks-support/testing-utilities/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@
The [Luigi Testing Utilities](https://github.com/SAP/luigi/tree/main/client-frameworks-support/testing-utilities) are a set of auxiliary functions used to enhance the user experience while testing Luigi-based micro frontends. The functions abstract away Luigi-specific logic from the tester so that it is easier for them to mock and assert Luigi functionality.

## LuigiMockUtil
Since version 2.9.0 this class contains certain utility helper functions needed when writing e2e tests with Cypress or Protractor. You can simply import this module into you project and then use an instance of it to test micro frontend functionality.
This class contains certain utility helper functions needed when writing e2e tests with different test frameworks. You can simply import this module into you project and then use an instance of it to test micro frontend functionality.
Before version 2.9.0 this class could only be used for [protractor-based](https://www.npmjs.com/package/protractor) e2e tests.
Since version 2.9.0 this class supports both Cypress and Protractor.
Since version 2.14.0 this class supports also Nightwatch, WebdriverIO and Puppeteer.

## How to use the library

**Prerequisites:**

_In order to use this utility library, you need to import LuigiMockModule into your Angular application's entry point. See more [here](https://docs.luigi-project.io/docs/framework-support-libraries/?section=luigicontextservice). You also have to install [Cypress](https://www.npmjs.com/package/cypress) or [Protractor](https://www.npmjs.com/package/protractor) locally as a dev dependency for your project. Bear in mind Protractor is deprecated in Angular since version 15._
_In order to use this utility library, you need to import LuigiMockModule into your Angular application's entry point - more details [here](https://docs.luigi-project.io/docs/framework-support-libraries/?section=luigicontextservice). You also have to install [Cypress](https://www.npmjs.com/package/cypress) or [Nightwatch](https://www.npmjs.com/package/nightwatch) or [WebdriverIO](https://www.npmjs.com/package/webdriverio) or [Puppeteer](https://www.npmjs.com/package/puppeteer) or [Protractor](https://www.npmjs.com/package/protractor) locally as a dev dependency for your project. Bear in mind Protractor is deprecated in Angular since version 15._


1. Import the library in the `package.json`:
Expand All @@ -20,14 +22,14 @@ npm install @luigi-project/testing-utilities -s

2. Once the library is imported and saved in your Angular project, you can now import the module `LuigiMockUtil` into your test:
```javascript
import { LuigiMockUtil } from "@luigi-project/testing-utilities";
import { LuigiMockUtil } from '@luigi-project/testing-utilities';
```

### Example how to use the library with Protractor

```javascript
import { browser } from 'protractor'; // <-- target e2e testing library
import { LuigiMockUtil } from "@luigi-project/testing-utilities";
import { LuigiMockUtil } from '@luigi-project/testing-utilities';

describe('Another test using protractor', () => {
let luigiMockUtil: LuigiMockUtil;
Expand All @@ -48,7 +50,7 @@ describe('Another test using protractor', () => {
### Example how to use the library with Cypress

```javascript
import { LuigiMockUtil } from "@luigi-project/testing-utilities";
import { LuigiMockUtil } from '@luigi-project/testing-utilities';

describe('Another test using cypress', () => {
let luigiMockUtil: LuigiMockUtil;
Expand Down Expand Up @@ -95,6 +97,154 @@ describe('Another test using cypress', () => {
});
```

### Example how to use the library with Nightwatch

```javascript
import { browser } from 'nightwatch'; // <-- target e2e testing library
import { LuigiMockUtil } from '@luigi-project/testing-utilities';

describe('Another test using nightwatch', function () {
const luigiMockUtil: LuigiMockUtil = new LuigiMockUtil(browser);

before((browser) => browser.navigateTo('http://localhost:4200'));

it('should mock path exists', async () => {
// Be sure '.pathExists' element is present
await browser.expect.element('.pathExists').to.be.present;
await browser.element('.pathExists').click().then(() => {
luigiMockUtil.mockPathExists('/test', false);
browser.execute(() => window.sessionStorage.getItem('luigiMockData'), [], function (result) {
expect(result.value).to.contains('{"pathExists":{"/test":false}}');
});
});
});

it('should mock context update', async () => {
const context = {ctxKey: 'ctxValue'};

await luigiMockUtil.mockContext(context);
// Wait until '#luigi-debug-vis-cnt' element is present
await browser.waitForElementPresent('#luigi-debug-vis-cnt', undefined, undefined, false, () => {
const wrapper = browser.expect.element('#luigi-debug-vis-cnt');

wrapper.to.be.present;
wrapper.text.to.contains('{"msg":"luigi.get-context","context":{"ctxKey":"ctxValue"}}');
});
});

after((browser) => browser.end());
});
```

### Example how to use the library with WebdriverIO

```javascript
import { browser } from '@wdio/globals'; // <-- target e2e testing library
import { LuigiMockUtil } from '@luigi-project/testing-utilities';

describe('Another test using webdriverio', () => {
const baseUrl = 'http://localhost:4200';
const defaultTimeout = { 'implicit': 500 };
let luigiMockUtil: LuigiMockUtil;

it('should mock path exists', async () => {
luigiMockUtil = new LuigiMockUtil(browser);

await browser.url(baseUrl);
// Be sure '.pathExists' element is present
await $('.pathExists').click().then(() => {
luigiMockUtil.mockPathExists('/test', false);
});
// Wait until session storage item is set
await browser.setTimeout(defaultTimeout);

const result = await browser.execute(() => window.sessionStorage.getItem('luigiMockData'));

await expect(result).toEqual('{"pathExists":{"/test":false}}');
});

it('should mock context update', async () => {
luigiMockUtil = new LuigiMockUtil(browser);

const context = {ctxKey: 'ctxValue'};

await browser.url(baseUrl);
await luigiMockUtil.mockContext(context);
// Wait until '#luigi-debug-vis-cnt' element is present
await browser.setTimeout(defaultTimeout);
await expect($('#luigi-debug-vis-cnt')).toHaveHTML(expect.stringContaining('{"msg":"luigi.get-context","context":{"ctxKey":"ctxValue"}}'));
});
});
```

### Example how to use the library with Puppeteer

```javascript
import * as puppeteer from 'puppeteer'; // <-- target e2e testing library
import { LuigiMockUtil } from '@luigi-project/testing-utilities';

let luigiMockUtil: LuigiMockUtil;
let browser: puppeteer.Browser;
let page: puppeteer.Page;

describe('Another test using puppeteer ->', () => {
beforeAll(async () => {
browser = await puppeteer.launch({
args: ['--no-sandbox'],
headless: false,
ignoreDefaultArgs: ['--disable-extensions'],
});
});

beforeEach(async () => {
page = await browser.newPage();
luigiMockUtil = new LuigiMockUtil(page);

await page?.goto('http://localhost:4200', {timeout: 0});
});

afterEach(async () => {
await page?.close();
});

afterAll(async () => {
await browser?.close();
});

it('should mock path exists', async () => {
// Be sure '.pathExists' element is present
await page.waitForSelector('.pathExists').then(async () => {
await expect(page.locator('.pathExists').wait()).toBeTruthy();

await page.click('.pathExists').then(async () => {
await luigiMockUtil.mockPathExists('/test', false);
// Wait until session storage item is set
await new Promise(resolve => setTimeout(resolve, 500));

const result = await page.evaluate(() => window.sessionStorage.getItem('luigiMockData'));

await expect(result).toContain('{"pathExists":{"/test":false}}');
});
});
});

it('should mock context update', async () => {
const context = {ctxKey: 'ctxValue'};

await luigiMockUtil.mockContext(context);
// Wait until '#luigi-debug-vis-cnt' element is present
await page.waitForSelector('#luigi-debug-vis-cnt').then(async () => {
const result = await page
.locator('#luigi-debug-vis-cnt div:nth-child(1)')
.map(div => div.innerText)
.wait();

expect(result).toContain('{"msg":"luigi.get-context","context":{"ctxKey":"ctxValue"}}');
});
});
});
```
#### Functions provided
- **mockContext**: Mocks the context by sending Luigi context messages with the desired mocked context as parameter.
- **mockPathExists**: This method serves as a mock for the Luigi Client `pathExists()` function. It is used in e2e tests when component being tested utilizes a call to `LuigiClient.linkManager().pathExists()`
Expand Down
2 changes: 1 addition & 1 deletion client-frameworks-support/testing-utilities/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@
"engines": {
"node": ">=18"
}
}
}
113 changes: 77 additions & 36 deletions client-frameworks-support/testing-utilities/src/luigi-mock-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,39 @@ export class LuigiMockUtil {
* Returns the global window object.
* @returns the glboal win object
*/

private getGlobalThis(): any {
return this.win || globalThis;
}


/**
* Parses the elements added by LuigiMockModule into the DOM and assigns them to the local this.messages variable
* @returns {Promise<void>} - A Promise that resolves when parsing is complete.
*/
async parseLuigiMockedMessages(): Promise<void> {
const window = this.getGlobalThis();
const getTextNodeValues = (): any[] => {
const debugCtn = window.getElementById('luigi-debug-vis-cnt');

return Array.from(debugCtn?.childNodes || []).map((item: any) => item.textContent || '');
};
let textElements: string[];

try {
const getTextNodeValues = () => {
const targetDocument = this.getGlobalThis();
const debugCtn = targetDocument.getElementById('luigi-debug-vis-cnt');
return Array.from(debugCtn?.childNodes || []).map((item: any) => item.textContent || '');
switch (true) {
case 'evaluate' in this.browser:
this.browser.evaluate(getTextNodeValues);
break;
case 'execute' in this.browser:
this.browser.execute(getTextNodeValues);
break;
case 'executeScript' in this.browser:
this.browser.executeScript(getTextNodeValues);
break;
default:
this.browser(getTextNodeValues);
break;
}

const textElements: string[] = this.browser.executeScript
? await this.browser.executeScript(getTextNodeValues)
: await this.browser(getTextNodeValues);
this.messages = textElements
.map((item: string) => {
try {
Expand All @@ -43,30 +55,40 @@ export class LuigiMockUtil {
}
})
.filter((item) => item !== undefined);
} catch (e) {
console.debug('Failed to parse luigi mocked messages: ', e);
} catch (error) {
console.debug('Failed to parse luigi mocked messages: ', error);
}
}

/**
* Mocks the context by sending luigi context messages with the desired mocked context as parameter.
* @param mockContext an object representing the context to be mocked
*/
mockContext = (mockContext: any): void => {
const context = mockContext;
const targetDocument = this.getGlobalThis();
const postMessageToLuigi = () => {
targetDocument.postMessage({ msg: 'luigi.get-context', context }, '*');
mockContext = (mockContext: Record<string, any>): void => {
const window = this.getGlobalThis();
const postMessageToLuigi = (context: Record<string, any>): Record<string, any> => {
window.postMessage({ msg: 'luigi.get-context', context }, '*');

return { ...context, windowMessage: 'isPosted' };
};

try {
if (this.browser.executeScript) {
this.browser.executeScript(postMessageToLuigi, context);
} else {
this.browser(postMessageToLuigi);
switch (true) {
case 'evaluate' in this.browser:
this.browser.evaluate(postMessageToLuigi, mockContext);
break;
case 'execute' in this.browser:
this.browser.execute(postMessageToLuigi, mockContext);
break;
case 'executeScript' in this.browser:
this.browser.executeScript(postMessageToLuigi, mockContext);
break;
default:
this.browser(postMessageToLuigi.bind(this, mockContext));
break;
}
} catch (e) {
console.debug('Failed to mock context: ', e);
} catch (error) {
console.debug('Failed to mock context: ', error);
}
};

Expand All @@ -87,30 +109,45 @@ export class LuigiMockUtil {
*
*/
mockPathExists = (path: string, exists: boolean): void => {
const targetDocument = this.getGlobalThis();
const window = this.getGlobalThis();
const mockContext: Record<string, boolean | string> = {path, exists};
/**
* Sets the path exists mock data in sessionStorage.
* @param {string} path - The path for which mock data is to be set.
* @param {boolean} exists - Boolean indicating whether the path exists.
* @returns {void}
* @returns {Object} - Object indicating session storage item.
*/
const setPathExistsMockData = () => {
targetDocument.sessionStorage.clear();
let pathExistsMockData = {
const setPathExistsMockData = (context: Record<string, boolean | string>): Record<string, any> => {
window.sessionStorage.clear();

const pathExistsMockData: Record<string, any> = {
pathExists: {
[path]: exists
[context['path'] as string]: context['exists']
}
};
targetDocument.sessionStorage.setItem('luigiMockData', JSON.stringify(pathExistsMockData));

window.sessionStorage.setItem('luigiMockData', JSON.stringify(pathExistsMockData));

return { ...pathExistsMockData, sessionItem: 'isStored' };
};

try {
if (this.browser.executeScript) {
this.browser.executeScript(setPathExistsMockData, path, exists);
} else {
this.browser(setPathExistsMockData, path, exists);
switch (true) {
case 'evaluate' in this.browser:
this.browser.evaluate(setPathExistsMockData, mockContext);
break;
case 'execute' in this.browser:
this.browser.execute(setPathExistsMockData, mockContext);
break;
case 'executeScript' in this.browser:
this.browser.executeScript(setPathExistsMockData, mockContext);
break;
default:
this.browser(setPathExistsMockData.bind(this, mockContext));
break;
}
} catch (e) {
console.debug('Failed to mock path exists: ', e);
} catch (error) {
console.debug('Failed to mock path exists: ', error);
}
};

Expand Down Expand Up @@ -153,22 +190,26 @@ export class LuigiMockUtil {
message.params &&
message.params.modal &&
message.params.modal.title;

if (msgExists) {
return message.params.modal.title === title;
}

return false;
});

if (indexFoundModalMessageWTitle >= 0) {
return true;
}

console.debug('Could not find modal with title: ', title);
return false;
}

/**
* Return list of messages, representing message elements added in the DOM for testing.
*/
getMSG() {
getMSG(): any[] {
return this.messages;
}
}
Loading

0 comments on commit 3654927

Please sign in to comment.