Home

Awesome

Knot.x Fragments Chrome Extension

Extends the Developer Tools, adding a sidebar that displays Fragments data associated with the selected DOM element.

<p align="center"> <img src="assets/images/preview.gif" alt="Knot.x Fragments Chrome Extension"/> </p>

It is a bridge between the business logic (domain) and the solution. Domain experts can easily verify the implementation of business logic, define new scenarios and deal with network problems (defining fallbacks) gradually. Developers and QAs can easily learn business logic, verify API responses/delays, and check page rendering performance issues.

See the tutorial and watch the live demo for more details.

How to use?

We are going to publish the extension on the Chrome Web Store platform. The acceptance process will take some time.

If you want to play with the extension using sample HTML Knot.x responses, see the instructions below:

How does it work?

Knot.x Fragments, when run in debug mode, injects information about fragments into the output. This information can then be read, parsed and displayed by various tools. Knot.x Fragments Chrome Extension is the official tool for this purpose.

Of course fragments' outputs can have various formats. Currently, Knot.x supports injecting debug information into both JSON and HTML responses.

When Knot.x HTTP response content type is application/json then it is parsed as JSON (according to RFC-4627). Otherwise, the extension interprets the response as an HTML.

Knot.x HTTP Server debug mode

Fragments debugging requires some Knot.x configuration changes. Knot.x provides Fragment Execution Log Consumer's implementations that write a fragment execution log to:

Contributors section

This section contains implementation details. We strongly encourage you to contribute!

Extension components

Extensions are made of different, but cohesive, components. Components can include background scripts, content scripts, an options page, UI elements and various logic files. (source)

Knot.x extension is made of such components as:

Extension components are created with web development technologies: HTML, CSS, and JavaScript. (source)

Knot.x extension is a single page application written in React with Redux as storage.

The src/js/content/content.js script parses the HTTP response body (per browser tab) and sends the message with fragments debug data to the src/js/background/background.js (which wraps the Redux storage). Then React components read the data directly from the Redux storage. See the diagram below.

Knotx.x HTTP Server -> HTTP response body -> CONTENT SCRIPT -> BACKGROUND SCRIPT -> REDUX -> COMPONENTS
                                                   ^
                                                   |
                                           parsing debug data

Parsing debug data

The chrome extension uses 3 parsers to read the fragment data in HTML.

•
└── helpers
    ├── graph
    │   └── declarationHelper.js
    ├── timeline
    │   └── declarationHelper.js
    └── nodesHelper.js
Nodes parser

The nodesHelper.js lists all the fragments on the page. It provides parseFragments method that takes an HTML element (the whole document, in practice) and returns a list of all fragments like this:

[
  {
    "debug": {}, // raw debug data from the fragment's script tag
    "nodes": [
      {
        "tag": "div",
        "selector": "css-selector-for-this-node-only"
      },
      // more nodes ...
    ]
  },
  // more fragments ...
]

It works by traversing all HTML nodes using Node Iterator and finding pairs of Knot.x comments that mark the beginning and end of a fragment. It then:

Graph parser

The graph/declarationHelper.js parses a given fragment's debug JSON (from the fragment's script tag) into a form understandable by (Vis.js Network)[https://visjs.github.io/vis-network/docs/network/], a library for displaying graphs. It provides constructGraph method that takes fragment's JSON as input and returns Vis.js-compatible datasets:

{
  "nodes": [
    {
      "id": "node-id",
      "label": "A node",
      "group": "success",
      "level": 0
    },
    // ...
  ],
  "edges": [
    {
      "from": "node-id",
      "to": "another-node-id",
      "label": "_success",
      "dashes": false,
      "font": {
        "color": "00CC00"
      },
      "color": "#000000"
    },
    // ...
  ]
}

It is then ready to be displayed in the form of a graph (specifically a tree unless there are composite nodes in the fragment).

Internally the parser consists of two phases:

Flattening of the graph transforms a structure like this:

{
  "id": "composite-node",
  // ...
  "on": {
    "_success": {
      "id": "next-node"
      // ...
    }
  },
  "subtasks": [
    {
      "id": "subtask-1",
      // ...
    },
    {
      "id": "subtask-2",
      // ...
    }
  ]
}

Into a graph like this:

{
  "id": "composite-node_virtual",
  // ...
  "on": {
    "_subtask_0": {
      "id": "subtask-1",
      // ...
      "on": {
        "_subtask_end": {
          "id": "composite-node_virtual_end",
          // ...
          "on": {
            "_success": { // original transition
              "id": "next-node",
              // ...
            }
          }
        }
      }
    },
    "_subtask_1": {
      "id": "subtask-2",
      // ...
      "on": {
        "_subtask_end": "composite-node_virtual_end" // note this is only an ID (!)
      }
    }
  }
}

An important thing to note is that, while all subtasks end with a transition to the composite-node_virtual_end node, only one of them (the deepest) contains an actual object in the transition. All other subtasks end with a transition into a string. It's termed a reference in the code and it's an ID of the actual node. It is like that to avoid duplication. Without it, the dataset-creation algorithm would treat transisions to the same node as transitions to multiple unique nodes. It'd result in parts of graph being copied multiple times, instead of multiple transitions transitioning to the same node.

Timeline parser

The timeline/declarationHelper.js parses a given fragment's debug JSON (from the fragment's script tag) into a form understandable by (Vis.js Timeline)[https://visjs.github.io/vis-timeline/docs/timeline/], a library for displaying Gantt charts. It provides constructTimeline method that takes fragment's JSON as input and returns Vis.js datasets.

Output looks like this:

{
  "items": [ // not an actual array, a vis.DataSet object
    {
      "id": "a-node",
      "start": 100000, // timestamp
      "end": 20000, // timestamp
      "content": "", // items have no labels in the currect implementation
      "group": "A group"
    },
    // ...
  ],
  "groups": [ // not an actual array, a vis.DataSet object
    {
      "id": "A group",
      "order": 0,
      "content": "A group",
      "nestedGroups": ["another group id", "and another one"] // null in case of no subgroups (can't be an empty array because of how Vis.js displays it)
    },
    // ...
  ]
}

Parser consists of the following phases:

React components

The components structure is:

•
└── App:
    ├── SidePanel
    │   ├── FragmenList
    │   │   └── FragmentListItem
    │   │       └── NodeList
    │   └── FragmentGannt
    │
    └── MainPanel
        └── Graph
            ├──  Timeline
            ├──  Legend
            │    └── LegendSection
            └──  NodeInfo

You can find interactive documentation for all components in our storybook.

To open the storybook follow the steps below:

Graph && timelines

We use vis.js library:

to visualise fragments task execution data. The following vis.js components are used:

Styling

We don't use any grid system to make our app beautiful. Everything is flex. To show and hide elements we try to use a react state, without saving this information in the redux store. SidePanelExpanded info is currently the only one exception.

To create styles we use styled-components. We follow the convention to create a style file next to js file.

•
├── exampleComponent.js
└── exampleComponent.style.js

some global styling and styling for render json markup we store in

/src/js/styling/globalStyle.js

Storage (Redux)

We use Redux as storage. It keeps details about:

Once loaded page data is stored in a map where:

Such storage solution makes it easy to analyse many pages at the same time, switching between them, and running many Chrome Dev Tools Console instances.

The example below presents how data is stored in Redux:

•
└── pageData:
    ├── 78: // tab id
    │   ├── fragments: [] // list of fragments
    │   ├── url: "https://example.com // page url
    │   ├── sidebarExpanded: true // side panel expanded switch
    │   └── renderedGraph: null // id of the currently selected fragment
    └── 110:
        └── ...

The pageData entry is created on page load and destroyed when we close the tab. If the page does not contain Knot.x fragments, fragments property is empty.

CI

The GitHub repository is integrated with Azure Pipelines (CI) to validate both new PRs and the master branch. Check the azure-pipelines.yml file for configuration details. So we check:

Azure pipline dashboard is avaiable here.

Testing

We believe that unit tests remain the best documentation. All React components, processing logic (helpers) and actions (such as a button click) are validated with unit tests. We use Jest and Enzyme frameworks to validate both components (React) with combination with mocked storage (Redux).

All JS files (components & helpers) have their own tests that are placed next to the tested sources. We follow the convention:

Additionally, we placed tests coverage verification in our CI. We use the jest-coverage tool for that. We decided to keep the coverage level at truly high levels (80 - 100%). It should enable future refactoring and code changes.

When tests are executed, then we generate the report (test-report.xml) file in the build/test folder. Moreover, there is the coverage directory that contains the index.html file with unit tests coverage report.

How to run tests?

Snapshots

We use 2 kinds of snapshot tests:

Both of them are implemented using storybook. You can find the config and diff output in src/js/snapshots/. To add new or update patterns (images or HTML markups) you have to remove old snapshots and create new by:

Visual snapshots use the dedicated jest config file (jest-snapshot.config.js).

Release

The application version is configured in /package.json file. Please note that the master branch should contain SNAPSHOT version.

Executing scripts below:

yarn run dev
yarn release minor

we produce the ZIP file in ./build containing all required distribution files. In the ./manifest.json file there is the released version. We use Webpack for releasing the extension.