GithubHelp home page GithubHelp logo

dmtrkovalenko / cypress-real-events Goto Github PK

View Code? Open in Web Editor NEW
722.0 12.0 68.0 3.72 MB

Fire native system events from Cypress.

License: MIT License

TypeScript 23.07% JavaScript 0.66% HTML 76.27%
cypress testing e2e native-events pupeteer cdp

cypress-real-events's People

Contributors

adamistheanswer avatar alirezamirian avatar andarist avatar arjunpatel17 avatar christine-pinto avatar denysdovhan avatar dependabot-preview[bot] avatar dependabot[bot] avatar dmtrkovalenko avatar dolevbitran avatar drecali avatar fernyb avatar illright avatar im-a-train avatar istateside avatar izhaki avatar jbard1 avatar joshwooding avatar kokogino avatar martinsik avatar mehmetyararvx avatar minilfat avatar mitchwd avatar nicholasboll avatar nicklemmon avatar noahgelman avatar piwysocki avatar rahulraj97 avatar silversonicaxel avatar snapris avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cypress-real-events's Issues

Implement in realClick, realSwipe, realTouch the support for percentages regarding x and y

It should be possible to interact with those commands also with percentages

as an example

cy.get("body").realClick({ x: `10%`, y: `12%` })

cy.get("body").realSwipe({ x: `10%`, y: `12%` })

cy.get("body").realTouch({ x: `10%`, y: `12%` })

or to keep intact the current x and y it could be

cy.get("body").realClick({ xPercent: `10%`, yPercent: `12%` })

cy.get("body").realSwipe({ xPercent: `10%`, yPercent: `12%` })

cy.get("body").realTouch({ xPercent: `10%`, yPercent: `12%` })

Implement swipe command

Swipe command

cy.get("element").realSwipe("left-to-right")
cy.get(element).realSwipe("right-to-left")
cy.get(element).realSwipe("bottom-to-top", { swipeWidth: 100 })
cy.get(element).realSwipe("top-to-bottom")

After I call the realHover() method over a menu and perform other actions, the menu is still opened

Hi @dmtrKovalenko I apologize for not posting it on the first place. I cannot show the actual behavior here as I cannot post anything related to my company code, but basically I have this code block

cy.get(this.menuNavBarMainReportsCSS).realHover(); // After this line is executed, the menu is still opened, like the hover didn't go away.

cy.get(this.menuNavBarItemClassicReportsCSS).realHover(); // I did this to hover over another element within that menu that expands when I hover in the first one, and it solves the problem.

cy.get(this.menuNavBarItemClassicReportsCSS).click();


Would this suffice? Thanks again for your time and apologies again

`.realHover()` effect is not captured in `cy.screenshot()`

Hi @dmtrKovalenko ,
first of all thank you for this amazing library!

I was trying to use realHover() in combination with Cypress cy.screenshot(). While I can easily see the effect of real hover in test GUI, the moment the screenshot is taken, that effect is gone.

On top of that, the hover effect doesn't persist even in Cypress' own DOM snapshots (in the GUI runner). Please see the GIF below:
https://share.getcloudapp.com/Z4uoRyem

Is there any way this effect can be persisted (forced?) or are there technical limitations obstructing that?
Thank you in advance and please let me know if I can provide more details.

Typescript support

Something seems off with typescript support in VS Code.
Following the recommended config results in "Property 'realHover' does not exist on type 'Chainable<JQuery>'." in VS Code but runs in cypress.

Vs Code appears to be looking at support.d.ts which only contains "export {};"

Changing the index.ts command from "import 'cypress-real-events/support';" to "import 'cypress-real-events/index';" resolves the error in VS Code but errors out when cypress runs.

Not able to type in a text box

AUT :- https://www.webdriveruniversity.com/Contact-Us/contactus.html
Cypress version : 6.0.0

describe('testing real event plugin', () =>{

    it('should ne able to test', () =>{

        cy.visit('https://www.webdriveruniversity.com/Contact-Us/contactus.html')
        cy.get('[name="first_name"]').realType('Automation')

    })
})

I found 2 issues:-

  1. I am not able to realtype
  2. As shown in the screenshot, code is realType, however, in runner, we see it as realPress

Screenshot 2020-11-27 at 12 31 52

Missing example/usage for options for realHover in the ReadMe

Current behaviour

The user wants to change options for realHover but they can't find any examples in the ReadMe/Documentation showing how to do that. Instead, the user needs to google for an example or needs to trial and error through their code.

Expected behaviour

The options for realHover have an example showing how to implement these options for realHover

ToDo

Add an example/usage (I can do that as I have been through the current behaviour and found a solution)

Error in firefox component testing

I have a simple component test and I am trying to do

cy.get('[name="number"]').realType('asd');

But I am getting the following error:
image
The only error in console is:

Uncaught TypeError: Cypress is undefined

This is working properly in Chrome.

Cypress version: 9.0.0
cypress-real-events version: 1.5.1

Expand the set of allowed characters for realType()

I would like to be able to type special language specific characters with realType. For example the german characters "ö", "ä", "ü".

cy.get('test').realType('Bär')

It would be nice if I would be able to extend the keyCodeDefinitions at runtime.

Implement cy.realDownload

Implement file downloading using real cypress event.

The main idea that it should download the file to the temporary directory in the file system (e.g. /tmp) or in any other directory user set to using Page.setDownloadBehavior.

This file will require a plugin registered.

cy.get("a#download").realDownload()
cy.get("a#download").realDownload("absolute/path")
cy.get("a#download").realDownload("./realative/to/the/project/path").then(file => {
  console.log(file.size)
  console.log(file.path)
  // and any other valuable info about file 
})

Test runner interferes with tabbing

When starting a test with cy.realPress('Tab'); it tabs through the cypress window before tabbing through the webpage.

Tried with:
Chrome 89
Node 12.20.1
cypress 6.8.0
cypress-real-events 1.3.0
yarn

RealPress not firing click events on button elements

Hi there, thanks for the great project! I was excited to find it since I ran into issues with Cypress not firing native events (and it not being on their roadmap). This tool is promising for keyboard accessibility testing! But as far as I can tell, the Enter key still doesn't work the same way browsers do as of version 1.5.1. Specifically, hitting the Enter key on a Button element should fire a native click event and handler callback function. In Cypress-Real-Events, the event is getting lost somewhere in the stack. The tests added in #69 work for putting text into an Input element, but not firing a callback function on an interactive element's click handler.

I looked into the code a bit and saw RealPress is successfully catching the user event and firing off Input.dispatchKeyEvent to the DevTools protocol. But on multiple repos where I tested it, the click handler attached to a button won't fire on the Enter key. This is important for writing keyboard accessibility tests that mimic user behavior, so we can differentiate between things like <div tabindex="0" onclick="handleClick()"> and <button onclick="handleClick()>, where the div will fail to function without a key handler but the button will click on Enter.

How to test

I've added a failing test for this behavior in my fork, I'd be happy to submit a PR for it: marcysutton@70f8b73

Here's a live test page I made where you can compare the user experience with your keyboard to what Cypress-Real-Events is doing: https://marcy.codes/testing/cypress-real-events/

Screenshots

In my test page, when a user presses the Enter key on a button element it appends text to a DIV:
Cypress Test page showing a focused button and event details appended to the screen

With Cypress-Real-Events, the text is never appended to the DIV when .realPress('Enter') is fired on the button:
Cypress App opened to a RealPress test showing the button failing to operate


If you have any tips (and if I'm not missing something obvious), I could potentially work on a PR. I'd love to get this working. Thanks!

Focus the AUT before actions

I think that for the majority of use cases (if not for all of them, maybe except tabbing) it makes sense to focus the AUT - without that some commands might not work properly.

I'm implementing global keyboard commands in our app so I want to use smth like to prepare the app for the assertion:

cy.realPress(['Shift', '1']);

However, this won't work because the AUT is not focused so my keyboard listener that lives in it has no chance of receiving this input.

I've tried this:

cy.get('body').focus();
cy.realPress(['Shift', '1']);

but it doesn't work even though after this we get:

cy.window().then(win => win.document.hasFocus() === true)

I've worked around my issue with:

cy.get('body').realClick({ position: 'topLeft' });
cy.realPress(['Shift', '1']);

but it feels that it could work "out of the box". I'm not entirely sure what exact command would be best to shift focus properly to the AUT before performing an action since a real click is not good in the sense that it could invoke some other, unrelated, stuff in the AUT.

Updating to work with Cypress v7.x

Hi:

We are currently beholden to Cypress v6.9.1 and cannot upgrade to the latest v7.x because of the following.

When running node v14.15.4, an error message is produced by cypress-real-events stating the following (in Node v12.21, this is a warning, not an error message):

npm ERR! Could not resolve dependency:
npm ERR! peer cypress@"^4.x || ^5.x || ^6.x" from [email protected]
npm ERR! node_modules/cypress-real-events
npm ERR!   dev cypress-real-events@"^1.3.0" from the root project

I haven't taken a look at cypress-real-events code to see why this version threshold exists; is there a reason why? Then, based off of that answer, what would it take to become compatible with Cypress v.7.x?

Thanks!

Typo in `cy.realMouseMove` code example in README

The code example for cy.realMouseMove is using cy,realMouseUp

Currently the code example for cy.realMouseMove in README.md is:

cy.get("sector").realMouseUp(50, 50, { position: "center" }); // moves by 50px x and y from center of sector

I think the correct example should be:

cy.get("sector").realMouseMove(50, 50, { position: "center" }); // moves by 50px x and y from center of sector

Link to relevant README section https://github.com/dmtrKovalenko/cypress-real-events#cyrealmousemove

Link to the specific text I'm referring to: https://github.com/dmtrKovalenko/cypress-real-events#:~:text=cy.get(%22sector%22).-,realmouseup(50%2C%2050,-%2C%20%7B%20position%3A%20%22center%22%20%7D)%3B%20%2F%2F%20moves

@dmtrKovalenko thank you for creating and maintaining such a useful Cypress plugin!

Please let me know if there are other issues or consideration and what the next steps should be. According to the contributing guidelines we should make an issue first. I was tempted to make a PR directly but I started with an issue and will follow the next steps when I hear back from you.

Triggering events on elements inside iframes calculate position incorrectly

I've been using cypress-real-events in the Cypress tests for our app. Our app renders a page inside of an <iframe>.

I've noticed that the events triggered on elements within that iframe are being triggered in the wrong positions, sometimes affecting elements other than the one we're calling the .realAction() commands on.

While looking into #26, I tracked down the problem here to getCypressElementCoordinates

  const { x, y, width, height } = htmlElement.getBoundingClientRect();
  const [posX, posY] = getPositionedCoordinates(
    x,
    y,
    width,
    height,
    position ?? "center"
  );


  return {
    x: appFrameX + (window.pageXOffset + posX) * appFrameScale,
    y: appFrameY + (window.pageYOffset + posY) * appFrameScale,
  };

This doesn't account for the case where the htmlElement is inside of an iframe nested inside of the appFrame. In that situation, htmlElement.getBoundingClientRect() returns coordinates relative to its closest iframe.

For us, that nested iframe was positioned 50px from the top of the app Frame, so all of the events triggered with this library were firing 50px higher than we wanted.

Library does not work on windows chrome

The library does not work on Windows chrome BUT seems to work on Windows headless and Linux.
Tested it with realHover and realClick.

Tried with:

  • Chrome 89
  • Node 12.18.3
  • cypress 6.5.0 & 6.8.0
  • typescript
  • cypress-real-events 1.2.0 & 1.3.0
  • yarn
  • vite

Cypress Iframe Handling - Failure to interact with Button

I am trying to E2E test an auth flow with Cypress that includes a third party method called BankID. BankId is integrated through three nested iframes that I can successfully access. However, when I type into the input field via cy.type('12345678912'), BankId does not register this as trusted events and never unlocks the submit button with the arrow.

When using your package via cy.realType('12345678912'), it actually succeeds in unlocking the submit button. However i can never successfully click the submit button, neither with .click() or even the package method .realClick().

The error is: "Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'."

I uploaded a sample repository with a minimal testing version here.

Any feedback or hints would be greatly appreciated :)

Here is the relevant code:

   
/// <reference types="cypress" />
import { skipOn } from '@cypress/skip-test'

describe('Recipe: blogs__iframes', () => {
  skipOn('firefox', () => {
    it('do it more generically', () => {
      const getIframeBody = (identifier) => {
        return cy
        .get(identifier)
        .its('0.contentDocument.body')
        .should('not.be.empty')
        .then(cy.wrap)
      }

      // Visiting the page index.html and getting iframe A
      cy.visit('index.html').contains('XHR in iframe')
      getIframeBody('iframe[data-cy="bankid"]').as('iframeA')

      cy.get('@iframeA').within(() => {
        getIframeBody('iframe[src="https://tools.bankid.no/bankid-test/auth"]').as('iframeB')

        cy.get('@iframeB').within(() => {
          getIframeBody('iframe[src^="https://csfe.bankid.no/CentralServerFEJS"]').as('iframeC')

          // Now we are in the right place and it finds the correct input element.
          // However, normal cypress command .type() fails and we have to use library cypress-real-events,
          // which provides an event firing system that works literally like in puppeteer
          cy.get('@iframeC').find('input[type="tel"]').should('be.visible').realType('12345678912')

          // But for the button below, this library now doesn't help anymore:
          // "Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'."
          cy.get('@iframeC').find('button[type="submit"]').should('be.visible').first().realClick()
        })
      })
    })
  })
})

Make it possible to perform actions while pressing a particular key or pointer button

To test some interactions we might have to do multiple things at once - like for example press space and move the pointer around while holding the left button of the mouse down to perform a "pan" in a canvas-based app (like in Figma, Excalidraw and more)

Should we introduce cy.realPressDown and cy.realPressUp to allow this? With this in place it should be possible to perform this sequence in a test - although it would be nice to also get some command for moving the pointer and this is related to #17

'Enter' functionality was not implemented correctly and does not work

Testing out the new Enter function, it is not working correctly.

The Enter information is being passed in via an object key: Link to code

The Enter key data needs to be a part of the top level object, not its' own object inside a key.

You can have a button that console logs a message when clicked and the new enter functionality will not work. Moving the enter data to the top level of the object fixes it

realTouch() Absolute Coordinates does not respect Viewport size

Hi,

I have the viewport size set in a Cypress test to cy.viewport(1280, 720);, however the realTouch() does not always have the correct absolute coordinates when the browser is resized - I believe this is because the coordinates are calculated based on the iframe rather than the Html or Body element of the page.

For example, if I run my test on a full page width, the calculated absolute coordinates are x: 1254 and y: 177. If I resize my window, the coordinates change to x:1221 and y:177, even though my viewport is locked to 1280x720px.

image

image

cy.press() is not a function

as per the document, cy.press() function exists
however, when I used, cypress throws error cy.press() saying is not a function

However, Intellisense, shows, cy.realPress() function.

May be its a typo in documentation.

Shift+Tab syntax

Hi, loving the plugin so far.

I have a question about the syntax for Shift+Tab. I can't seem to get the syntax right without throwing an error. Can you add an example of multi-key pressing to the README?

Compatibility with Cypress 9

Would it be possible to relax the version constraint in package.json to include Cypress 9 or does some work need to be done first to make this library compatible?

Use more predictable units for offset positions

@dmtrKovalenko has mentioned that the used coordinate system is not intuitive to Cypress users. I partially agree with that - I think though that the internal coordinate system is mostly hidden from users and they don't have to care about it that much because cypress-real-events is taking care of the heavy-lifting and computes the screen positions of AUT elements.
#198 (comment)

I think though that there is one thing that could be done to make interacting with this library more intuitive and preditable

Currently, if we do smth like:

cy.get('div').realClick({ position: {  x: 5, y: 5 } })

then the computed position will use { x: 5, y: 5 } as a screen offset of the element's position. I don't think this is what users expect though, for the majority of use cases. This is especially less-than-ideal as the final position is depending on the screen size and can vary based on the whole window being resized or devtools being open etc

I propose to redefine what this offset means - IMHO this should be an offset from the el.getBoundingClientRect(). That way we'd end up with more consistent results, independent from the root viewport size.

The change should be pretty trivial (but breaking!) - we'd have to essentially shift the logic from getPositionedCoordinates to getElementPositionXY (the latter should probably be renamed in the process).

Issue installing with Typescript

Getting SyntaxError: 'import' and 'export' may appear only with 'sourceType: module' when installing on cypress/ typescript

Installed using npm install cypress-real-events --save-dev

Added import "cypress-real-events/support"; to cypress/support/index.js

"compilerOptions": {

"target": "ES6",                         
"module": "commonjs",                    
 "lib": [
    "DOM",
    "ES6"
], 

Is there a different setup required? Tried moving import statement to support/commands.js, code compiles but get 'cy.realpress is not a function when method called.

Implement drag and drop

Implement the native dnd command. It should look like

cy.get("button").realDnD("button.button2")
cy.get("button").realDnD({ x: 100, y: 100 })

The automated release is failing 🚨

🚨 The automated release from the main branch failed. 🚨

I recommend you give this issue a high priority, so other packages depending on you could benefit from your bug fixes and new features.

You can find below the list of errors reported by semantic-release. Each one of them has to be resolved in order to automatically publish your package. I’m sure you can resolve this 💪.

Errors are usually caused by a misconfiguration or an authentication problem. With each error reported below you will find explanation and guidance to help you to resolve it.

Once all the errors are resolved, semantic-release will release your package the next time you push a commit to the main branch. You can also manually restart the failed CI job that runs semantic-release.

If you are not sure how to resolve this, here is some links that can help you:

If those don’t help, or if this issue is reporting something you think isn’t right, you can always ask the humans behind semantic-release.


No npm token specified.

An npm token must be created and set in the NPM_TOKEN environment variable on your CI environment.

Please make sure to create an npm token and to set it in the NPM_TOKEN environment variable on your CI environment. The token must allow to publish to the registry https://registry.npmjs.org/.


Good luck with your project ✨

Your semantic-release bot 📦🚀

Using `.real[action]` commands forces a `scrollIntoView`, regardless of Cypress scrollBehavior/force options

Hey folks. Big fan of the library, helped me get through some tricky issues with :hover styling in Cypress tests.

I've been tracking down an issue with one of my tests using cy.realHover. The page scrolls, and then the wrong element ends up getting hovered. I'm not 100%, but I think the wrong element is being hovered because the element has changed positions after scrolling?

Regardless of the wrong element being hovered- I didn't expect that realHover would force the element to be scrolled into view. realHover (and I believe the other action commands) uses getCypressElementCoordinates, which calls htmlElement.scrollIntoView.

By default, Cypress does scroll the page when you interact with elements on the page, but you can customize that behavior by passing { scrollBehavior: string|false } in Cypress >=6, or { force: true } in Cypress <=5. You can also set the scrollBehavior as a global option in your cypress.json file..

I would expect cypress-real-events to respect that scrollBehavior option if given, or at the least, not force a scroll on elements that are already visible on the page.

I am not familiar at all with how the CDB protocol works, so there may be a reason this is required that I'm unaware of.

I've got a quick patch I'm working on that I'll put up as a PR soon. Happy to close it if there's something I'm missing here.

Make pointer-related commands "dual"

It would be nice if I could just do this:

cy.realClick({ x: 10, y: 10 })

instead of having to do this

cy.get('body').realClick({ x: 10, y: 10 })

`realMouseMove()` uses hardcoded position "topLeft"

I think the cy.realMouseMove() command has a hardcoded position as seen below.

const basePosition= getCypressElementCoordinates(
subject,
"topLeft",
options.scrollBehavior
);

I tried to use it with position: center as in the code example below but the event happened in the same hardcoded topLeft position.

cy.get("sector").realMouseMove(50, 50, { position: "center" }); // moves by 50px x and y from center of sector

I made Cypress test cases for all Cypress positions listed here:

export type Position =
| "topLeft"
| "top"
| "topRight"
| "left"
| "center"
| "right"
| "bottomLeft"
| "bottom"
| "bottomRight"

Here is the pattern for my Cypress tests.:

cy.get("body")
  .realMouseDown()
  .realMouseMove(20, 10, { position: "topLeft" })
  .realMouseMove(30, 20, { position: "topLeft" })
  .realMouseUp();
cy.get("body")
  .realMouseDown()
  .realMouseMove(20, 10, { position: "top" })
  .realMouseMove(30, 20, { position: "top" })
  .realMouseUp();
cy.get("body")
  .realMouseDown()
  .realMouseMove(-20, 10, { position: "topRight" })
  .realMouseMove(-30, 20, { position: "topRight" })
  .realMouseUp();

Outcomes

Ideal Current
image image

Currently, the marks are all overlapping. Ideally, they should all be in different positions according to the options given to cy.realMouseMove()

Real Press Arrow keys not working on Mac

Confirmed by a coworker. Using the latest version of cypress-real-events and cypress, Using Real Press with the arrow keys down not function properly in a Mac environment.

Real Click not working on GithubAction

I recently added the dependency to my project which is running on github action , for some reason locally works fine, but on the github action when runs, then I got this error:

CypressError: cy.click() can only be called on a single element. Your subject contained 2 elements. Pass { multiple: true } if you want to serially click each element.

"react": "^16.13.1",
"cypress": "^5.3.0",

any ideas??

Implement a .realActive() method

Implement a method for showing an html element's active state. cy.realActive() will put the element in the active state to check for active styling/behavior.

Use case: To check css active state styles/behavior of an element.

cy.get('button').realActive()

Real click not working when scrolling is used

I am testing a video where the click on the middle of the scrubber bar is meant to be clicked by .realClick({ position: 'center' }); and test that the current time of the video has elapsed a certain amount.

However, with the inclusion of this.focusRef.current.scrollTop = 0; which scrolls the entire component, the test becomes flaky and 30-40/100 times the scrubber bar is focused but can't click be on.

Tried with:
Chrome 89
Node 12.20.1
cypress 6.8.0
cypress-real-events 1.3.0
yarn

Implement cy.realUpload

Task cy.realUploadwill upload file from the file system. It should work similar to https://github.com/abramenal/cypress-file-upload/ but upload the file using CDP.

This command will require a plugin to be registered.

It is possible using a puppeteer, so it should (https://dev.to/sonyarianto/practical-puppeteer-how-to-upload-a-file-programatically-4nm4) and we should be able to attach input file using DOM.setFileInputFiles CDP command instead of javascript-based approach

cy.get("input").realUpload("filename.jpg")

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.