-
Notifications
You must be signed in to change notification settings - Fork 131
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add getBladeCoverage and assertBladeCoverage utilities (#1864)
Co-authored-by: Kamlesh Chandnani <kamlesh.chandnani@gmail.com>
- Loading branch information
1 parent
bdbdf42
commit 469b2d7
Showing
6 changed files
with
250 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
'@razorpay/blade': minor | ||
--- | ||
|
||
feat: add `getBladeCoverage` and `assertBladeCoverage` utilities | ||
|
||
Read more about it in the [Blade Coverage documentation](http://blade.razorpay.com/?path=/story/utils-blade-coverage--page). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
/* eslint-disable @typescript-eslint/no-explicit-any */ | ||
export function getBladeCoverage(): { | ||
bladeCoverage: number; | ||
totalNodes: number; | ||
bladeNodes: number; | ||
}; | ||
|
||
export function assertBladeCoverage({ | ||
page, | ||
expect, | ||
threshold, | ||
}: { | ||
page: any; | ||
expect: any; | ||
threshold?: number; | ||
}): Promise<void>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { Meta } from '@storybook/addon-docs'; | ||
|
||
<Meta title="Utils/Blade Coverage" /> | ||
|
||
# Blade Coverage Utils | ||
|
||
Blade coverage measures the percentage of a page built with Blade. It does so by calculating the DOM nodes built with Blade vs Non-Blade. All the apps within Razorpay org that are using Blade have the coverage script integrated and you can check the coverage for your app on Amplitude. [Blade Coverage Dashboard](https://app.amplitude.com/analytics/razorpay-mobile/dashboard/texx2y4). | ||
|
||
You can utilize the following utils to measure the coverage in different stages of your app development workflow. | ||
|
||
> These functions are designed for web applications and should be used in a browser environment. | ||
## `assertBladeCoverage` | ||
|
||
This utility function asserts that the calculated blade coverage meets a specified threshold. | ||
|
||
Parameters: | ||
- **`page`:** Playwright page object. | ||
- **`expect`:** The `expect` function from `@playwright/test`. | ||
- **`threshold` (optional):** Minimum threshold for blade coverage (default is 70). | ||
|
||
### Usage | ||
|
||
- Ensure that you are using Blade v10.22.0 or above and playwright is properly set up. | ||
- Import and use `assertBladeCoverage` in your test files. Adjust the threshold based on your coverage requirements. | ||
|
||
```js dark | ||
import { test, expect } from '@playwright/test'; | ||
import { assertBladeCoverage } from '@razorpay/blade/coverageUtils'; | ||
|
||
test.describe.parallel('Test Home @flow=home', () => { | ||
test('should have blade coverage more than 70% @priority=normal', async ({ page }) => { | ||
await page.goto('/'); | ||
|
||
await assertBladeCoverage({ page, expect, threshold: 70 }); | ||
}); | ||
}); | ||
``` | ||
|
||
- Execute your tests using the Playwright Test runner. | ||
|
||
```bash dark | ||
npx playwright test | ||
``` | ||
|
||
- Once your tests are passing and blade coverage is meeting expectations, you're good to go! | ||
## `getBladeCoverage` | ||
> Consider installing the [Blade Coverage Chrome Extension](https://chromewebstore.google.com/detail/blade-coverage-extension/cpmmcebielcknjffelmpbcbgkcjapipp). This extension provides a convenient way to visualize and analyze the blade coverage directly in the Chrome browser. We internally use the below utility function. | ||
This utility function calculates the blade usage coverage in percentage of the DOM elements on a web page. | ||
```js dark | ||
import { getBladeCoverage } from '@razorpay/blade/coverageUtils'; | ||
const { bladeCoverage, totalNodes, bladeNodes } = getBladeCoverage(); | ||
``` | ||
- **`bladeCoverage`:** The percentage of blade nodes in the total nodes. | ||
- **`totalNodes`:** Total number of DOM nodes. | ||
- **`bladeNodes`:** Number of blade nodes. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
const getBladeCoverage = (): { | ||
bladeCoverage: number; | ||
totalNodes: number; | ||
bladeNodes: number; | ||
} => { | ||
/** | ||
* Checks if DOM node is hidden or not | ||
*/ | ||
const isElementHidden = (element: Element): boolean => { | ||
if (element.parentElement && isElementHidden(element.parentElement)) { | ||
return true; | ||
} | ||
if (!(element instanceof HTMLElement)) { | ||
return false; | ||
} | ||
if (element.hidden) { | ||
return true; | ||
} | ||
const style = getComputedStyle(element); | ||
return style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0'; | ||
}; | ||
|
||
/** | ||
* Checks if DOM node is a media element or not | ||
*/ | ||
const isMediaElement = (element: Element): boolean => { | ||
const mediaTags = ['img', 'video', 'audio', 'source', 'picture']; | ||
return mediaTags.includes(element.tagName.toLowerCase()); | ||
}; | ||
|
||
/** | ||
* Checks if DOM element is empty or not | ||
*/ | ||
const isElementEmpty = (element: Element): boolean => { | ||
if (!element) return true; | ||
if (!element.childNodes.length) { | ||
return true; | ||
} | ||
return false; | ||
}; | ||
|
||
const allDomElements = document.querySelectorAll('body *'); | ||
|
||
const bladeNodeElements = []; | ||
const totalNodeElements = []; | ||
|
||
allDomElements.forEach((elm) => { | ||
if (isElementHidden(elm)) return; | ||
if (isElementEmpty(elm)) return; | ||
if (isMediaElement(elm)) return; | ||
|
||
// skip svg nodes but not blade icons | ||
const closestSvgNode = elm.closest('svg'); | ||
// if this is a blade icon then add it | ||
if (elm.tagName.toLocaleLowerCase() === 'svg' && elm.hasAttribute('data-blade-component')) { | ||
bladeNodeElements.push(elm); | ||
totalNodeElements.push(elm); | ||
return; | ||
} | ||
// if it's a svg node inside a blade icon then skip it | ||
if (closestSvgNode?.getAttribute('data-blade-component') === 'icon') { | ||
return; | ||
} | ||
// if it's a svg node but not a blade icon then skip it | ||
if (closestSvgNode && !elm.hasAttribute('data-blade-component')) { | ||
return; | ||
} | ||
|
||
totalNodeElements.push(elm); | ||
|
||
// If element has data-blade-component add it | ||
if (elm.hasAttribute('data-blade-component')) { | ||
bladeNodeElements.push(elm); | ||
} | ||
}); | ||
|
||
const totalNodes = totalNodeElements.length; | ||
const bladeNodes = bladeNodeElements.length; | ||
let bladeCoverage = Number(((bladeNodes / totalNodes) * 100).toFixed(2)); | ||
// NaN guard | ||
if (totalNodes === 0) { | ||
bladeCoverage = 0; | ||
} | ||
|
||
return { | ||
bladeCoverage, | ||
totalNodes, | ||
bladeNodes, | ||
}; | ||
}; | ||
|
||
const assertBladeCoverage = async ({ | ||
page, | ||
expect, | ||
threshold = 70, | ||
}: { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
page: any; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
expect: any; | ||
threshold: number; | ||
}): Promise<void> => { | ||
const { bladeCoverage } = await page.evaluate((coverageFnStr: string) => { | ||
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func | ||
const calculateBladeCoverage = new Function(`return (${coverageFnStr})()`); | ||
return calculateBladeCoverage(); | ||
}, getBladeCoverage.toString()); | ||
|
||
expect(bladeCoverage).toBeGreaterThanOrEqual(threshold); | ||
}; | ||
|
||
module.exports = { getBladeCoverage, assertBladeCoverage }; |