-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Serialize in-memory attributes into deep DOM clone #208
Conversation
c64ebe5
to
37b82c5
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding a few TODOs and helpful comments
src/percy-agent-client/dom.ts
Outdated
* that only lives in memory (not an asset or in the DOM). | ||
* | ||
*/ | ||
private serializeCssOm(documentClone: any) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be nice to do some perf testing here (re #188). I think we're going to have to bite the perf cost here of iterating through each in-memory stylesheet each time.
Maybe in the future we can do some caching. Feels dangerous though, I'd rather capture the CSSOM each time to be 100% sure it's correct.
$style.type = 'text/css' | ||
$style.setAttribute('data-percy-cssom-serialized', 'true') | ||
$style.innerHTML = serializedStyles | ||
// TODO, it'd be better if we appended it right after the ownerNode in the clone |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is something that's nice to have, but not critical for this PR to get through.
expect(domSnapshot).to.contain('type="checkbox" checked') | ||
expect(domSnapshot).to.contain('type="radio" checked') | ||
expect(domSnapshot).to.contain('test textarea value') | ||
const $ = cheerio.load(domSnapshot) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cheerio makes working with the string of the DOM much easier by giving a jQuery API to transverse the string. This was needed because the contain
method checks to make sure that exact string matches. So the order of type="radio" checked
needs to be exact (which it isn't) to pass.
Cheerio allows us to query for that element in the DOM and check its attributes.
* our API. | ||
* | ||
*/ | ||
snapshotString(): string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd wondering about being more specific here domSnapshotString
or some other name - to avoid possible confusion with Percy's Snapshots. I.e. in the near future we think each Percy Snapshot may have multiple DOM Snapshots.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the name domSnapshotString
.
Though, it is on the DOM
class:
let dom = new DOM(document);
// this
dom.snapshotString()
// vs
dom.domSnapshotString()
I don't have a strong feel either way
* This will change the customers DOM and have a possible impact on the | ||
* customers application. | ||
* | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May also want to include comments above saying currently we're only using it to add data-percy-element-id's to inputs which should be safe. Also that this mutation can apply to the same original DOM multiple times within one test/snapshot set. This can be dangerous, which is why we suggest strictly minimizing changes made here.
Introduce cheerio so we can parse the DOM string in the node tests. This is better than searching the entire DOM string for strings that we expect to be there. This gives us a jQuery like interface to pick elements & attributes out.
This is to ensure the stub has never been called before we actually expect it to
3fc3ad7
to
e9b29aa
Compare
These methods are already mutating the cloned DOM. There's no need to capture it in a variable and return it from each stabilize method. It gives the illustion that it's not causing side-effects, but it really is. Let's make that explicit
|
||
it('serializes the input value', () => { | ||
expect($domString('#name').attr('value')).to.equal('Bob Boberson') | ||
expect(dom.clonedDOM.querySelector('#name').attributes.value.value).to.equal('Bob Boberson') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
expect(dom.clonedDOM.querySelector('#name').attributes.value.value).to.equal('Bob Boberson') | |
expect(dom.clonedDOM.querySelector('#name').getAttribute('value')).to.equal('Bob Boberson') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🍍 LGTM
## [0.5.1](v0.5.0...v0.5.1) (2019-05-31) ### Bug Fixes * Serialize in-memory attributes into deep DOM clone ([#208](#208)) ([56d76bd](56d76bd))
Small background
Percy Agent is the underlying package that powers most of our SDKs now. It provides a CLI to handle creating the builds (among other features), a node process for processing the DOM, a way to capture assets, and provides the JS we use to actually capture the DOM from the customer's browser.
There is some state within the page that we need to capture but aren't present in the DOM we take from the browser (form element values, CSS Object Model, Canvas elements, etc).
In order for us to properly capture all of the state within the customer's browser, we need to serialize those values into the DOM we are capturing.
The problem
At this time we're modifying the DOM within the customer's browser to capture that state. This can be harmful to their applications state. You can see how this works now here:
percy-agent/src/percy-agent-client/percy-agent.ts
Lines 58 to 95 in 593d82a
First, we take the DOM that's passed from the snapshot method and serialize those in-memory values into the customers DOM. Then we take a clone of it after we've modified it.
There are two main problems with the current approach:
The fix
We can solve the two issues listed above by cloning the DOM that's passed from the
snapshot
method as soon as we get it and then serializing the in-memory values into the DOM clone. This would require us to change how the current system works by passing both the original DOM and the clone that we've taken.Approach
Isolate DOM transforms into a single class
This PR isolates all of the DOM transformations we make to a single DOM class & pulls the two different
serialize-*.ts
files into that class. This will make it easy to contain, manage, and document each DOM transformation that happens. The steps that we take to capture this information are a little confusing and nuanced. I’d like to make it as clear as possible the type of transforms that are happening.This will also make testing MUCH easier. It will also make it possible for us to store any future errors we want to log on the class, and then pass that class along as debug output.
Inputs
To capture the values from the inputs we will still need to modify their DOM but it's only adding a Percy data attribute with a guid. This is because we need to be able to locate the various inputs in the original DOM and serialize them into the DOM clone. We cannot rely on their selectors or attributes. It also must be done before the clone is taken so the guid is present and the consistent in both DOM trees.
I believe this is as non-invasive as we can get with this approach.
CSSOM
There are two small changes to get stable CSSOM serialization across snapshots:
parentNode
of the CSSOM’sownerNode
. We do not want to serialize these styles into the CSSOM’sownerNode
because it will break how the various libraries modify the CSSOM.We have learned in the past that removing styles that were serialized into the CSSOM’s
ownerNode
will calldeleteRule()
in the CSSOM, which removes all styles present in the application (while also breaking the library that generates the styles). We want to avoid adding or changing any styles that the CSSOM owns.Test coverage
This needs to be thoroughly tested. We should make sure:
snapshotString()
always returns a string with a doctypedomTransformation
works as expecteddomTransformation
errors are captured and do not error the whole suiteOpen TODOs
I'm not sure any of these are blocking, but could be convinced they are.
any
.parentNode
as the CSSOM's styleownerNode