Skip to content

Commit

Permalink
feat: add getBladeCoverage and assertBladeCoverage utilities (#1864)
Browse files Browse the repository at this point in the history
Co-authored-by: Kamlesh Chandnani <kamlesh.chandnani@gmail.com>
  • Loading branch information
snitin315 and kamleshchandnani authored Dec 14, 2023
1 parent bdbdf42 commit 469b2d7
Show file tree
Hide file tree
Showing 6 changed files with 250 additions and 2 deletions.
7 changes: 7 additions & 0 deletions .changeset/young-mice-join.md
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).
16 changes: 16 additions & 0 deletions packages/blade/coverageUtils.d.ts
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>;
62 changes: 62 additions & 0 deletions packages/blade/docs/utils/coverge.stories.mdx
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.
9 changes: 8 additions & 1 deletion packages/blade/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"tokens.native.js",
"utils.d.ts",
"utils.js",
"utils.native.js"
"utils.native.js",
"coverageUtils.d.ts"
],
"keywords": [
"design system",
Expand Down Expand Up @@ -69,6 +70,12 @@
"production": "./build/lib/web/production/utils/index.js",
"default": "./build/lib/web/production/utils/index.js"
}
},
"./coverageUtils": {
"default": {
"types": "./coverageUtils.d.ts",
"default": "./build/lib/web/production/utils/bladeCoverage.js"
}
}
},
"scripts": {
Expand Down
46 changes: 45 additions & 1 deletion packages/blade/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,48 @@ const getWebConfig = (inputs) => {
};
};

// This is a special config for bladeCoverage.ts
// We need to bundle it as cjs so that we can use it in playwright tests https://github.com/microsoft/playwright/issues/23662
const getBladeCoverageConfig = (inputs) => {
const platform = 'web';
const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development';

return {
input: inputs,
// Since we individually bundle each export category (components, tokens, utils)
// it is possible that some function of `utils` is used in `components` but
// rollup will have no idea about it and tree shake it away.
// So we disable tree shaking, this should not cause any issues since consumers will do their own tree shaking.
treeshake: false,
output: [
{
dir: `${outputRootDirectory}/${libDirectory}/${platform}/${mode}`,
format: 'cjs',
sourcemap: true,
preserveModules: true,
preserveModulesRoot: 'src',
},
],
plugins: [
pluginReplace({
__DEV__: process.env.NODE_ENV !== 'production',
preventAssignment: true,
}),
pluginPeerDepsExternal(),
depsExternalPlugin({ externalDependencies }),
pluginResolve({ extensions: webExtensions }),
pluginCommonjs(),
pluginBabel({
exclude: 'node_modules/**',
babelHelpers: 'runtime',
envName: 'production',
extensions: webExtensions,
}),
aliases,
],
};
};

const getNativeConfig = (inputs) => {
const platform = 'native';

Expand Down Expand Up @@ -229,12 +271,14 @@ const config = () => {
const components = 'src/components/index.ts';
const tokens = 'src/tokens/index.ts';
const utils = 'src/utils/index.ts';
const bladeCoverage = 'src/utils/bladeCoverage.ts';
if (framework === 'REACT') {
return [
getWebConfig(components),
getWebConfig(tokens),
getWebConfig(utils),
// Unfortunately we cannot just simply copy the tsc emited declarations and put it on build dir,
getBladeCoverageConfig(bladeCoverage),
// Unfortunately we cannot just simply copy the tsc emitted declarations and put it on build dir,
// because moduleSuffixes will cause typescript to resolve the d.ts files based on the user's tsconfig.json
// which will cause the build to fail because the user's tsconfig.json does not have the moduleSuffixes
// So we opt for the older approach of bundling the d.ts files as index.d.ts and index.native.d.ts and place it on build dir.
Expand Down
112 changes: 112 additions & 0 deletions packages/blade/src/utils/bladeCoverage.ts
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 };

0 comments on commit 469b2d7

Please sign in to comment.