Cross origin iframe support (#1035)

* Add `recordCrossOriginIframe` setting

* Set up messaging between iframes

* should emit full snapshot event from iframe as mutation event

* this.mirror was dropped on attachIframe

* should use unique id for child of iframe

* Cross origin iframe recording in `yarn live-stream`

* Root iframe check thats supported by firefox

* Live stream: Inject script in all frames

* Record same origin and cross origin iframes differently

* Should map Input events correctly

* Turn on other tests

* Fix compatibility with newer puppeteer

* puppeteer vs 12 seems stable without to many changes needed

* normalize port numbers in snapshots

* Handle scroll and ViewportResize events in cross origin iframe

* Correctly map cross origin mutations

* Map selection events for cross origin iframes

* Map canvas mutations for cross origin iframes

* Update snapshot to include canvas events

* Skip all meta events

* Support custom events as best we can in cross origin iframes

* Use earliest version of puppeteer that works with cross origin live-stream

* Map mouse/touch interaction events

* Update snapshots for correctly mapped click events

* Tweak tests for new puppeteer version

* Map MediaInteraction correctly for cross origin iframes

* Make tests consistent between high and low dpi devices

* Make test less flaky

* Make test less flaky

* Make test less flaky

* Make test less flaky

* Add support for styles in cross origin iframes

* Map traditional stylesheet mutations on cross origin iframes

* Add todo

* Add iframe mirror

* Get iframe manager to use iframe mirrors internally

* Rename `IframeMirror` to `CrossOriginIframeMirror`

* Setup basic cross origin canvas webrtc streaming

* Clean up removed canvas elements

* reset style mirror on new full snapshot

* Fix cross origin canvas webrtc streaming

* Make emit optional

* Run tests on github actions

* Upload image artifacts from failed tests

* Use newer github actions

* Test: hopefully adding more wait will fix it

* add extra wait

* Fix image snapshot tests

* Make tests run with new puppeteer version

* upgrade eslint-plugin-jest

* Chore: Remove travis ci as ci's running on github actions

* Chore: Support recording cross origin iframe in repl

* Force developers to update the cross origin iframe mapping when adding new events

https://github.com/rrweb-io/rrweb/pull/1035#discussion_r1012516277

* Document cross origin iframe recording

* Docs: cross origin iframes recording methods

* Docs: AI translated, cross origin iframe recording

* rename getParentId to getId

* Migrate to @rrweb/types

* Run on pull request

* doc: improve Chinese doc

* Rename `parentId` to `Id`

Co-authored-by: Mark-Fenng <f18846188605@gmail.com>
This commit is contained in:
Justin Halsall
2026-04-01 12:00:00 +08:00
committed by GitHub
parent df5d547446
commit 2cd3d2afe9
38 changed files with 7362 additions and 1038 deletions

View File

@@ -4014,8 +4014,27 @@ exports[`record integration tests mutations should work when blocked class is un
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\n\\",
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 7
},
{
\\"type\\": 2,
\\"tagName\\": \\"style\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"#b-class, #b-class-2 { height: 33px; width: 200px; }\\",
\\"isStyle\\": true,
\\"id\\": 9
}
],
\\"id\\": 8
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\",
\\"id\\": 10
}
],
\\"id\\": 3
@@ -4023,7 +4042,7 @@ exports[`record integration tests mutations should work when blocked class is un
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\n\\",
\\"id\\": 8
\\"id\\": 11
},
{
\\"type\\": 2,
@@ -4033,7 +4052,7 @@ exports[`record integration tests mutations should work when blocked class is un
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 10
\\"id\\": 13
},
{
\\"type\\": 2,
@@ -4043,40 +4062,22 @@ exports[`record integration tests mutations should work when blocked class is un
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 12
\\"id\\": 15
}
],
\\"id\\": 11
\\"id\\": 14
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 13
\\"id\\": 16
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 14
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 15
},
{
\\"type\\": 2,
\\"tagName\\": \\"h1\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n Verify that block class bugs are fixed\\\\n \\",
\\"id\\": 17
}
],
\\"id\\": 16
\\"id\\": 17
},
{
\\"type\\": 3,
@@ -4085,15 +4086,33 @@ exports[`record integration tests mutations should work when blocked class is un
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"tagName\\": \\"h1\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"Verify that block class bugs are fixed\\",
\\"id\\": 20
}
],
\\"id\\": 19
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 20
\\"id\\": 21
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 22
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 23
},
{
\\"type\\": 2,
@@ -4105,7 +4124,7 @@ exports[`record integration tests mutations should work when blocked class is un
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 22
\\"id\\": 25
},
{
\\"type\\": 2,
@@ -4117,7 +4136,7 @@ exports[`record integration tests mutations should work when blocked class is un
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 24
\\"id\\": 27
},
{
\\"type\\": 2,
@@ -4127,91 +4146,91 @@ exports[`record integration tests mutations should work when blocked class is un
{
\\"type\\": 3,
\\"textContent\\": \\"VISIBLE\\",
\\"id\\": 26
\\"id\\": 29
}
],
\\"id\\": 25
\\"id\\": 28
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 27
\\"id\\": 30
}
],
\\"id\\": 23
\\"id\\": 26
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 28
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 29
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 30
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 31
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 32
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 33
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 34
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 32
\\"id\\": 35
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"class\\": \\"rr-block\\",
\\"rr_width\\": \\"1904px\\",
\\"rr_height\\": \\"21px\\"
\\"rr_width\\": \\"200px\\",
\\"rr_height\\": \\"33px\\"
},
\\"childNodes\\": [],
\\"id\\": 33
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 34
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 35
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 36
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 37
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 37
\\"id\\": 38
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 39
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 40
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 38
\\"id\\": 41
},
{
\\"type\\": 2,
@@ -4223,49 +4242,49 @@ exports[`record integration tests mutations should work when blocked class is un
{
\\"type\\": 3,
\\"textContent\\": \\"MUTATE\\",
\\"id\\": 40
\\"id\\": 43
}
],
\\"id\\": 39
\\"id\\": 42
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 41
\\"id\\": 44
}
],
\\"id\\": 21
\\"id\\": 24
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 42
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 43
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 44
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 45
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 46
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 47
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 48
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 46
\\"id\\": 49
},
{
\\"type\\": 2,
@@ -4277,7 +4296,7 @@ exports[`record integration tests mutations should work when blocked class is un
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 48
\\"id\\": 51
},
{
\\"type\\": 2,
@@ -4289,7 +4308,7 @@ exports[`record integration tests mutations should work when blocked class is un
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 50
\\"id\\": 53
},
{
\\"type\\": 2,
@@ -4299,91 +4318,91 @@ exports[`record integration tests mutations should work when blocked class is un
{
\\"type\\": 3,
\\"textContent\\": \\"VISIBLE\\",
\\"id\\": 52
\\"id\\": 55
}
],
\\"id\\": 51
\\"id\\": 54
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 53
\\"id\\": 56
}
],
\\"id\\": 49
\\"id\\": 52
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 54
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 55
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 56
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 57
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 58
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 59
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 60
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 58
\\"id\\": 61
},
{
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {
\\"class\\": \\"rr-block\\",
\\"rr_width\\": \\"1904px\\",
\\"rr_height\\": \\"21px\\"
\\"rr_width\\": \\"200px\\",
\\"rr_height\\": \\"33px\\"
},
\\"childNodes\\": [],
\\"id\\": 59
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 60
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 61
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 62
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 63
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 63
\\"id\\": 64
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 65
},
{
\\"type\\": 2,
\\"tagName\\": \\"br\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 66
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 64
\\"id\\": 67
},
{
\\"type\\": 2,
@@ -4395,23 +4414,23 @@ exports[`record integration tests mutations should work when blocked class is un
{
\\"type\\": 3,
\\"textContent\\": \\"MUTATE\\",
\\"id\\": 66
\\"id\\": 69
}
],
\\"id\\": 65
\\"id\\": 68
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 67
\\"id\\": 70
}
],
\\"id\\": 47
\\"id\\": 50
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n\\\\n \\",
\\"id\\": 68
\\"id\\": 71
},
{
\\"type\\": 2,
@@ -4421,18 +4440,18 @@ exports[`record integration tests mutations should work when blocked class is un
{
\\"type\\": 3,
\\"textContent\\": \\"SCRIPT_PLACEHOLDER\\",
\\"id\\": 70
\\"id\\": 73
}
],
\\"id\\": 69
\\"id\\": 72
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n\\",
\\"id\\": 71
\\"id\\": 74
}
],
\\"id\\": 9
\\"id\\": 12
}
],
\\"id\\": 2
@@ -4452,7 +4471,7 @@ exports[`record integration tests mutations should work when blocked class is un
\\"data\\": {
\\"source\\": 2,
\\"type\\": 1,
\\"id\\": 39
\\"id\\": 42
}
},
{
@@ -4460,7 +4479,7 @@ exports[`record integration tests mutations should work when blocked class is un
\\"data\\": {
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 39
\\"id\\": 42
}
},
{
@@ -4468,7 +4487,7 @@ exports[`record integration tests mutations should work when blocked class is un
\\"data\\": {
\\"source\\": 2,
\\"type\\": 0,
\\"id\\": 39
\\"id\\": 42
}
},
{
@@ -4476,7 +4495,7 @@ exports[`record integration tests mutations should work when blocked class is un
\\"data\\": {
\\"source\\": 2,
\\"type\\": 2,
\\"id\\": 39
\\"id\\": 42
}
},
{
@@ -4486,7 +4505,7 @@ exports[`record integration tests mutations should work when blocked class is un
\\"texts\\": [],
\\"attributes\\": [
{
\\"id\\": 33,
\\"id\\": 36,
\\"attributes\\": {
\\"class\\": \\"notB\\"
}
@@ -4495,50 +4514,19 @@ exports[`record integration tests mutations should work when blocked class is un
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 23,
\\"parentId\\": 26,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 72
}
},
{
\\"parentId\\": 72,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 73
}
},
{
\\"parentId\\": 73,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"button\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 74
}
},
{
\\"parentId\\": 74,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"I1I2 VISIBLE\\",
\\"id\\": 75
}
},
{
\\"parentId\\": 72,
\\"nextId\\": 73,
\\"parentId\\": 75,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"div\\",
@@ -4563,71 +4551,13 @@ exports[`record integration tests mutations should work when blocked class is un
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"I1I1 VISIBLE\\",
\\"textContent\\": \\"I1I2 VISIBLE\\",
\\"id\\": 78
}
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 1,
\\"id\\": 65
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 6,
\\"id\\": 39
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 65
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 0,
\\"id\\": 65
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 2,
\\"id\\": 65
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [
},
{
\\"id\\": 59,
\\"attributes\\": {
\\"class\\": \\"notB\\"
}
}
],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 49,
\\"nextId\\": null,
\\"parentId\\": 75,
\\"nextId\\": 76,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"div\\",
@@ -4641,7 +4571,7 @@ exports[`record integration tests mutations should work when blocked class is un
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"tagName\\": \\"button\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 80
@@ -4651,25 +4581,83 @@ exports[`record integration tests mutations should work when blocked class is un
\\"parentId\\": 80,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"button\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"type\\": 3,
\\"textContent\\": \\"I1I1 VISIBLE\\",
\\"id\\": 81
}
},
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 1,
\\"id\\": 68
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 6,
\\"id\\": 42
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 5,
\\"id\\": 68
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 0,
\\"id\\": 68
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 2,
\\"type\\": 2,
\\"id\\": 68
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [
{
\\"parentId\\": 81,
\\"id\\": 62,
\\"attributes\\": {
\\"class\\": \\"notB\\"
}
}
],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 52,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"I1I2 VISIBLE\\",
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 82
}
},
{
\\"parentId\\": 79,
\\"nextId\\": 80,
\\"parentId\\": 82,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"div\\",
@@ -4694,9 +4682,40 @@ exports[`record integration tests mutations should work when blocked class is un
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"I1I1 VISIBLE\\",
\\"textContent\\": \\"I1I2 VISIBLE\\",
\\"id\\": 85
}
},
{
\\"parentId\\": 82,
\\"nextId\\": 83,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"div\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 86
}
},
{
\\"parentId\\": 86,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"button\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 87
}
},
{
\\"parentId\\": 87,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 3,
\\"textContent\\": \\"I1I1 VISIBLE\\",
\\"id\\": 88
}
}
]
}

View File

@@ -1905,168 +1905,6 @@ exports[`record captures stylesheet rules 1`] = `
]"
`;
exports[`record captures stylesheets in iframes that are still loading 1`] = `
"[
{
\\"type\\": 4,
\\"data\\": {
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
}
},
{
\\"type\\": 2,
\\"data\\": {
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 1,
\\"name\\": \\"html\\",
\\"publicId\\": \\"\\",
\\"systemId\\": \\"\\",
\\"id\\": 2
},
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 4
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 6
},
{
\\"type\\": 2,
\\"tagName\\": \\"input\\",
\\"attributes\\": {
\\"type\\": \\"text\\",
\\"size\\": \\"40\\"
},
\\"childNodes\\": [],
\\"id\\": 7
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
\\"id\\": 8
},
{
\\"type\\": 2,
\\"tagName\\": \\"iframe\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 9
}
],
\\"id\\": 5
}
],
\\"id\\": 3
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"adds\\": [
{
\\"parentId\\": 9,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"link\\",
\\"attributes\\": {
\\"rel\\": \\"stylesheet\\",
\\"href\\": \\"blob:null\\"
},
\\"childNodes\\": [],
\\"rootId\\": 10,
\\"id\\": 13
}
],
\\"rootId\\": 10,
\\"id\\": 12
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"rootId\\": 10,
\\"id\\": 14
}
],
\\"rootId\\": 10,
\\"id\\": 11
}
],
\\"compatMode\\": \\"BackCompat\\",
\\"id\\": 10
}
}
],
\\"removes\\": [],
\\"texts\\": [],
\\"attributes\\": [],
\\"isAttachIframe\\": true
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"adds\\": [],
\\"removes\\": [],
\\"texts\\": [],
\\"attributes\\": [
{
\\"id\\": 13,
\\"attributes\\": {
\\"_cssText\\": \\"body { color: pink; }\\"
}
}
]
}
}
]"
`;
exports[`record captures stylesheets in iframes with \`blob:\` url 1`] = `
"[
{
@@ -2211,126 +2049,6 @@ exports[`record captures stylesheets in iframes with \`blob:\` url 1`] = `
]"
`;
exports[`record captures stylesheets that are still loading 1`] = `
"[
{
\\"type\\": 4,
\\"data\\": {
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
}
},
{
\\"type\\": 2,
\\"data\\": {
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 1,
\\"name\\": \\"html\\",
\\"publicId\\": \\"\\",
\\"systemId\\": \\"\\",
\\"id\\": 2
},
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 4
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 6
},
{
\\"type\\": 2,
\\"tagName\\": \\"input\\",
\\"attributes\\": {
\\"type\\": \\"text\\",
\\"size\\": \\"40\\"
},
\\"childNodes\\": [],
\\"id\\": 7
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\\\n \\\\n \\",
\\"id\\": 8
}
],
\\"id\\": 5
}
],
\\"id\\": 3
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 4,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"link\\",
\\"attributes\\": {
\\"rel\\": \\"stylesheet\\",
\\"href\\": \\"blob:null\\"
},
\\"childNodes\\": [],
\\"id\\": 9
}
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"adds\\": [],
\\"removes\\": [],
\\"texts\\": [],
\\"attributes\\": [
{
\\"id\\": 9,
\\"attributes\\": {
\\"_cssText\\": \\"body { color: pink; }\\"
}
}
]
}
}
]"
`;
exports[`record captures stylesheets with \`blob:\` url 1`] = `
"[
{
@@ -2910,6 +2628,522 @@ exports[`record is safe to checkout during async callbacks 1`] = `
]"
`;
exports[`record loading stylesheets captures stylesheets in iframes that are still loading 1`] = `
"[
{
\\"type\\": 4,
\\"data\\": {
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
}
},
{
\\"type\\": 2,
\\"data\\": {
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 1,
\\"name\\": \\"html\\",
\\"publicId\\": \\"\\",
\\"systemId\\": \\"\\",
\\"id\\": 2
},
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {
\\"lang\\": \\"en\\"
},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 5
},
{
\\"type\\": 2,
\\"tagName\\": \\"meta\\",
\\"attributes\\": {
\\"charset\\": \\"UTF-8\\"
},
\\"childNodes\\": [],
\\"id\\": 6
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 7
},
{
\\"type\\": 2,
\\"tagName\\": \\"meta\\",
\\"attributes\\": {
\\"http-equiv\\": \\"X-UA-Compatible\\",
\\"content\\": \\"IE=edge\\"
},
\\"childNodes\\": [],
\\"id\\": 8
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 9
},
{
\\"type\\": 2,
\\"tagName\\": \\"meta\\",
\\"attributes\\": {
\\"name\\": \\"viewport\\",
\\"content\\": \\"width=device-width, initial-scale=1.0\\"
},
\\"childNodes\\": [],
\\"id\\": 10
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 11
},
{
\\"type\\": 2,
\\"tagName\\": \\"title\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"Hello World!\\",
\\"id\\": 13
}
],
\\"id\\": 12
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 14
}
],
\\"id\\": 4
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 15
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n Hello world!\\\\n \\\\n\\\\n\\",
\\"id\\": 17
},
{
\\"type\\": 2,
\\"tagName\\": \\"iframe\\",
\\"attributes\\": {},
\\"childNodes\\": [],
\\"id\\": 18
}
],
\\"id\\": 16
}
],
\\"id\\": 3
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"adds\\": [
{
\\"parentId\\": 18,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 1,
\\"name\\": \\"html\\",
\\"publicId\\": \\"\\",
\\"systemId\\": \\"\\",
\\"rootId\\": 19,
\\"id\\": 20
},
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {
\\"lang\\": \\"en\\"
},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"rootId\\": 19,
\\"id\\": 23
},
{
\\"type\\": 2,
\\"tagName\\": \\"meta\\",
\\"attributes\\": {
\\"charset\\": \\"UTF-8\\"
},
\\"childNodes\\": [],
\\"rootId\\": 19,
\\"id\\": 24
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"rootId\\": 19,
\\"id\\": 25
},
{
\\"type\\": 2,
\\"tagName\\": \\"meta\\",
\\"attributes\\": {
\\"http-equiv\\": \\"X-UA-Compatible\\",
\\"content\\": \\"IE=edge\\"
},
\\"childNodes\\": [],
\\"rootId\\": 19,
\\"id\\": 26
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"rootId\\": 19,
\\"id\\": 27
},
{
\\"type\\": 2,
\\"tagName\\": \\"meta\\",
\\"attributes\\": {
\\"name\\": \\"viewport\\",
\\"content\\": \\"width=device-width, initial-scale=1.0\\"
},
\\"childNodes\\": [],
\\"rootId\\": 19,
\\"id\\": 28
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"rootId\\": 19,
\\"id\\": 29
},
{
\\"type\\": 2,
\\"tagName\\": \\"title\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"Hello World!\\",
\\"rootId\\": 19,
\\"id\\": 31
}
],
\\"rootId\\": 19,
\\"id\\": 30
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"rootId\\": 19,
\\"id\\": 32
}
],
\\"rootId\\": 19,
\\"id\\": 22
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"rootId\\": 19,
\\"id\\": 33
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n Hello world!\\\\n \\\\n\\\\n\\",
\\"rootId\\": 19,
\\"id\\": 35
}
],
\\"rootId\\": 19,
\\"id\\": 34
}
],
\\"rootId\\": 19,
\\"id\\": 21
}
],
\\"id\\": 19
}
}
],
\\"removes\\": [],
\\"texts\\": [],
\\"attributes\\": [],
\\"isAttachIframe\\": true
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 22,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"link\\",
\\"attributes\\": {
\\"rel\\": \\"stylesheet\\",
\\"href\\": \\"http://localhost:3030/html/assets/style.css\\"
},
\\"childNodes\\": [],
\\"rootId\\": 19,
\\"id\\": 36
}
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"adds\\": [],
\\"removes\\": [],
\\"texts\\": [],
\\"attributes\\": [
{
\\"id\\": 36,
\\"attributes\\": {
\\"_cssText\\": \\"body { color: pink; }\\"
}
}
]
}
}
]"
`;
exports[`record loading stylesheets captures stylesheets that are still loading 1`] = `
"[
{
\\"type\\": 4,
\\"data\\": {
\\"href\\": \\"about:blank\\",
\\"width\\": 1920,
\\"height\\": 1080
}
},
{
\\"type\\": 2,
\\"data\\": {
\\"node\\": {
\\"type\\": 0,
\\"childNodes\\": [
{
\\"type\\": 1,
\\"name\\": \\"html\\",
\\"publicId\\": \\"\\",
\\"systemId\\": \\"\\",
\\"id\\": 2
},
{
\\"type\\": 2,
\\"tagName\\": \\"html\\",
\\"attributes\\": {
\\"lang\\": \\"en\\"
},
\\"childNodes\\": [
{
\\"type\\": 2,
\\"tagName\\": \\"head\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 5
},
{
\\"type\\": 2,
\\"tagName\\": \\"meta\\",
\\"attributes\\": {
\\"charset\\": \\"UTF-8\\"
},
\\"childNodes\\": [],
\\"id\\": 6
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 7
},
{
\\"type\\": 2,
\\"tagName\\": \\"meta\\",
\\"attributes\\": {
\\"http-equiv\\": \\"X-UA-Compatible\\",
\\"content\\": \\"IE=edge\\"
},
\\"childNodes\\": [],
\\"id\\": 8
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 9
},
{
\\"type\\": 2,
\\"tagName\\": \\"meta\\",
\\"attributes\\": {
\\"name\\": \\"viewport\\",
\\"content\\": \\"width=device-width, initial-scale=1.0\\"
},
\\"childNodes\\": [],
\\"id\\": 10
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 11
},
{
\\"type\\": 2,
\\"tagName\\": \\"title\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"Hello World!\\",
\\"id\\": 13
}
],
\\"id\\": 12
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 14
}
],
\\"id\\": 4
},
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n \\",
\\"id\\": 15
},
{
\\"type\\": 2,
\\"tagName\\": \\"body\\",
\\"attributes\\": {},
\\"childNodes\\": [
{
\\"type\\": 3,
\\"textContent\\": \\"\\\\n Hello world!\\\\n \\\\n\\\\n\\",
\\"id\\": 17
}
],
\\"id\\": 16
}
],
\\"id\\": 3
}
],
\\"id\\": 1
},
\\"initialOffset\\": {
\\"left\\": 0,
\\"top\\": 0
}
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"texts\\": [],
\\"attributes\\": [],
\\"removes\\": [],
\\"adds\\": [
{
\\"parentId\\": 4,
\\"nextId\\": null,
\\"node\\": {
\\"type\\": 2,
\\"tagName\\": \\"link\\",
\\"attributes\\": {
\\"rel\\": \\"stylesheet\\",
\\"href\\": \\"http://localhost:3030/html/assets/style.css\\"
},
\\"childNodes\\": [],
\\"id\\": 18
}
}
]
}
},
{
\\"type\\": 3,
\\"data\\": {
\\"source\\": 0,
\\"adds\\": [],
\\"removes\\": [],
\\"texts\\": [],
\\"attributes\\": [
{
\\"id\\": 18,
\\"attributes\\": {
\\"_cssText\\": \\"body { color: pink; }\\"
}
}
]
}
}
]"
`;
exports[`record should record scroll position 1`] = `
"[
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -76,7 +76,9 @@ describe('e2e webgl', () => {
const hideMouseAnimation = async (p: puppeteer.Page) => {
await p.addStyleTag({
content: '.replayer-mouse-tail{display: none !important;}',
content: `.replayer-mouse-tail{display: none !important;}
html, body { margin: 0; padding: 0; }
iframe { border: none; }`,
});
};
@@ -90,7 +92,9 @@ describe('e2e webgl', () => {
await waitForRAF(page);
const snapshots: eventWithTime[] = await page.evaluate('window.snapshots');
const snapshots: eventWithTime[] = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
page = await browser.newPage();
@@ -108,9 +112,8 @@ describe('e2e webgl', () => {
`);
await waitForRAF(page);
const element = await page.$('iframe');
const frameImage = await element!.screenshot();
const frameImage = await page!.screenshot();
await waitForRAF(page);
expect(frameImage).toMatchImageSnapshot();
});
@@ -122,8 +125,11 @@ describe('e2e webgl', () => {
getHtml.call(this, 'canvas-webgl-image.html', { recordCanvas: true }),
);
await waitForRAF(page);
await page.waitForTimeout(100);
const snapshots: eventWithTime[] = await page.evaluate('window.snapshots');
const snapshots: eventWithTime[] = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
page = await browser.newPage();
@@ -143,9 +149,7 @@ describe('e2e webgl', () => {
await page.evaluate(`replayer.play(500);`);
await waitForRAF(page);
const element = await page.$('iframe');
const frameImage = await element!.screenshot();
const frameImage = await page!.screenshot();
expect(frameImage).toMatchImageSnapshot();
});
});

View File

@@ -0,0 +1,3 @@
body {
color: pink;
}

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Audio</title>
</head>
<body>
<h1>1 minute of silence</h1>
<audio controls>
<source src="assets/1-minute-of-silence.mp3" type="audio/mpeg" />
Your browser does not support the audio element.
</audio>
</body>
</html>

View File

@@ -1,88 +1,92 @@
<head>
<title>Uber Application for Codegen Testing</title>
<style>
#b-class,
#b-class-2 {
height: 33px;
width: 200px;
}
</style>
</head>
<body>
<script>
function mutate1() {
const bClassDiv = document.getElementById("b-class");
bClassDiv.className = "notB";
function mutate1() {
const bClassDiv = document.getElementById('b-class');
bClassDiv.className = 'notB';
const removeBlockedButton = document.getElementById("remove");
removeBlockedButton.remove();
const removeBlockedButton = document.getElementById('remove');
removeBlockedButton.remove();
const visibleCollection = document.getElementsByClassName("visible");
const i1Div = document.createElement("div");
const i1i1Div = document.createElement("div");
const i1i2Div = document.createElement("div");
const visibleCollection = document.getElementsByClassName('visible');
const i1Div = document.createElement('div');
const i1i1Div = document.createElement('div');
const i1i2Div = document.createElement('div');
const i1i1Button = document.createElement("button");
i1i1Button.innerHTML = "I1I1 VISIBLE";
i1i1Div.appendChild(i1i1Button);
const i1i1Button = document.createElement('button');
i1i1Button.innerHTML = 'I1I1 VISIBLE';
i1i1Div.appendChild(i1i1Button);
const i1i2Button = document.createElement("button");
i1i2Button.innerHTML = "I1I2 VISIBLE";
i1i2Div.appendChild(i1i2Button);
const i1i2Button = document.createElement('button');
i1i2Button.innerHTML = 'I1I2 VISIBLE';
i1i2Div.appendChild(i1i2Button);
i1Div.appendChild(i1i1Div);
i1Div.appendChild(i1i2Div);
visibleCollection[0].appendChild(i1Div);
}
function mutate2() {
const bClassDiv = document.getElementById("b-class-2");
bClassDiv.className = "notB";
i1Div.appendChild(i1i1Div);
i1Div.appendChild(i1i2Div);
visibleCollection[0].appendChild(i1Div);
}
function mutate2() {
const bClassDiv = document.getElementById('b-class-2');
bClassDiv.className = 'notB';
const removeBlockedButton = document.getElementById("remove2");
const innerButton = document.createElement("button");
innerButton.innerHTML = "INNER BLOCKED";
removeBlockedButton.appendChild(innerButton)
removeBlockedButton.remove();
const removeBlockedButton = document.getElementById('remove2');
const innerButton = document.createElement('button');
innerButton.innerHTML = 'INNER BLOCKED';
removeBlockedButton.appendChild(innerButton);
removeBlockedButton.remove();
const visibleCollection = document.getElementsByClassName("visible2");
const i1Div = document.createElement("div");
const i1i1Div = document.createElement("div");
const i1i2Div = document.createElement("div");
const visibleCollection = document.getElementsByClassName('visible2');
const i1Div = document.createElement('div');
const i1i1Div = document.createElement('div');
const i1i2Div = document.createElement('div');
const i1i1Button = document.createElement("button");
i1i1Button.innerHTML = "I1I1 VISIBLE";
i1i1Div.appendChild(i1i1Button);
const i1i1Button = document.createElement('button');
i1i1Button.innerHTML = 'I1I1 VISIBLE';
i1i1Div.appendChild(i1i1Button);
const i1i2Button = document.createElement("button");
i1i2Button.innerHTML = "I1I2 VISIBLE";
i1i2Div.appendChild(i1i2Button);
const i1i2Button = document.createElement('button');
i1i2Button.innerHTML = 'I1I2 VISIBLE';
i1i2Div.appendChild(i1i2Button);
i1Div.appendChild(i1i1Div);
i1Div.appendChild(i1i2Div);
visibleCollection[0].appendChild(i1Div);
}
i1Div.appendChild(i1i1Div);
i1Div.appendChild(i1i2Div);
visibleCollection[0].appendChild(i1Div);
}
</script>
<br/>
<h1>
Verify that block class bugs are fixed
</h1>
<br/>
<br />
<h1>Verify that block class bugs are fixed</h1>
<br />
<div class="first">
<div class="visible">
<button>VISIBLE</button>
</div>
<br/><br/><br/>
<br /><br /><br />
<div class="rr-block" id="b-class">
<button id="remove">BLOCKED</button>
</div>
<br/><br/><br/>
<br /><br /><br />
<button onclick="mutate1()">MUTATE</button>
</div>
<br/><br/><br/>
<br /><br /><br />
<div class="second">
<div class="visible2">
<button>VISIBLE</button>
</div>
<br/><br/><br/>
<br /><br /><br />
<div class="rr-block" id="b-class-2">
<button id="remove2">BLOCKED</button>
</div>
<br/><br/><br/>
<br /><br /><br />
<button onclick="mutate2()">MUTATE</button>
</div>
</body>

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello World!</title>
</head>
<body>
Hello world!
</body>
</html>

View File

@@ -73,7 +73,9 @@ describe('record integration tests', function (this: ISuite) {
await page.type('textarea', 'textarea test');
await page.select('select', '1');
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -91,7 +93,9 @@ describe('record integration tests', function (this: ISuite) {
p.appendChild(document.createElement('span'));
});
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -111,7 +115,9 @@ describe('record integration tests', function (this: ISuite) {
p.innerText = 'mutated';
});
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -129,7 +135,9 @@ describe('record integration tests', function (this: ISuite) {
document.body.setAttribute('test', 'true');
});
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -146,7 +154,9 @@ describe('record integration tests', function (this: ISuite) {
await page.evaluate(
'document.getElementById("select2-drop").setAttribute("style", document.getElementById("select2-drop").style.cssText + "color:black !important")',
);
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -180,7 +190,9 @@ describe('record integration tests', function (this: ISuite) {
await waitForRAF(page);
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -191,7 +203,9 @@ describe('record integration tests', function (this: ISuite) {
await page.type('.rr-ignore', 'secret');
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -209,7 +223,9 @@ describe('record integration tests', function (this: ISuite) {
await page.type('textarea', 'textarea test');
await page.select('select', '1');
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -233,7 +249,9 @@ describe('record integration tests', function (this: ISuite) {
await page.type('input[type="password"]', 'password');
await page.select('select', '1');
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -250,7 +268,9 @@ describe('record integration tests', function (this: ISuite) {
await page.type('input[type="password"]', 'secr3t');
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -268,7 +288,9 @@ describe('record integration tests', function (this: ISuite) {
await page.type('textarea', 'textarea test');
await page.select('select', '1');
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -281,7 +303,9 @@ describe('record integration tests', function (this: ISuite) {
await page.evaluate(`document.getElementById('text').innerText = '1'`);
await page.click('#text');
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -301,7 +325,9 @@ describe('record integration tests', function (this: ISuite) {
nextElement.parentNode!.insertBefore(el, nextElement);
});
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -310,13 +336,19 @@ describe('record integration tests', function (this: ISuite) {
await page.goto('about: blank');
await page.setContent(getHtml.call(this, 'blocked-unblocked.html'));
const elements1 = await page.$x('/html/body/div[1]/button');
const elements1 = (await page.$x(
'/html/body/div[1]/button',
)) as puppeteer.ElementHandle<HTMLButtonElement>[];
await elements1[0].click();
const elements2 = await page.$x('/html/body/div[2]/button');
const elements2 = (await page.$x(
'/html/body/div[2]/button',
)) as puppeteer.ElementHandle<HTMLButtonElement>[];
await elements2[0].click();
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -334,7 +366,9 @@ describe('record integration tests', function (this: ISuite) {
p.removeChild(span);
div.appendChild(span);
});
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -349,7 +383,9 @@ describe('record integration tests', function (this: ISuite) {
document.body.appendChild(div);
div.appendChild(span);
});
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -358,7 +394,9 @@ describe('record integration tests', function (this: ISuite) {
await page.goto('about:blank');
await page.setContent(getHtml.call(this, 'react-styled-components.html'));
await page.click('.toggle');
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -371,7 +409,9 @@ describe('record integration tests', function (this: ISuite) {
}),
);
await waitForRAF(page);
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
for (const event of snapshots) {
if (event.type === EventType.FullSnapshot) {
visitSnapshot(event.data.node, (n) => {
@@ -393,7 +433,9 @@ describe('record integration tests', function (this: ISuite) {
}),
);
await page.waitForTimeout(50);
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -406,7 +448,9 @@ describe('record integration tests', function (this: ISuite) {
}),
);
await waitForRAF(page);
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -425,7 +469,9 @@ describe('record integration tests', function (this: ISuite) {
}
});
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -483,11 +529,15 @@ describe('record integration tests', function (this: ISuite) {
document.body.appendChild(iframe);
});
await waitForRAF(page);
await page.frames()[1].evaluate(() => {
console.log('from iframe');
});
await waitForRAF(page);
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -504,7 +554,9 @@ describe('record integration tests', function (this: ISuite) {
await page.waitForTimeout(50);
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -519,7 +571,9 @@ describe('record integration tests', function (this: ISuite) {
await page.waitForSelector('img'); // wait for image to get added
await waitForRAF(page); // wait for image to be captured
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -534,7 +588,9 @@ describe('record integration tests', function (this: ISuite) {
await page.waitForTimeout(50); // wait for image to get added
await waitForRAF(page); // wait for image to be captured
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -555,7 +611,9 @@ describe('record integration tests', function (this: ISuite) {
await page.waitForTimeout(50); // wait for image to get added
await waitForRAF(page); // wait for image to be captured
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -603,7 +661,9 @@ describe('record integration tests', function (this: ISuite) {
});
await page.waitForTimeout(50);
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -646,7 +706,9 @@ describe('record integration tests', function (this: ISuite) {
});
await waitForRAF(page); // wait till browser sent snapshots
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -674,7 +736,9 @@ describe('record integration tests', function (this: ISuite) {
});
await waitForRAF(page); // wait for snapshot to be updated
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -706,7 +770,9 @@ describe('record integration tests', function (this: ISuite) {
});
await waitForRAF(page); // wait till browser sent snapshots
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -746,7 +812,9 @@ describe('record integration tests', function (this: ISuite) {
});
await waitForRAF(page); // wait till browser sent snapshots
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -759,7 +827,9 @@ describe('record integration tests', function (this: ISuite) {
}),
);
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -773,7 +843,9 @@ describe('record integration tests', function (this: ISuite) {
}),
);
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
@@ -794,7 +866,9 @@ describe('record integration tests', function (this: ISuite) {
p.innerText = 'mutated';
});
const snapshots = await page.evaluate('window.snapshots');
const snapshots = (await page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
});

View File

@@ -11,7 +11,14 @@ import {
styleSheetRuleData,
selectionData,
} from '@rrweb/types';
import { assertSnapshot, launchPuppeteer, waitForRAF } from './utils';
import {
assertSnapshot,
getServerURL,
launchPuppeteer,
startServer,
waitForRAF,
} from './utils';
import type { Server } from 'http';
interface ISuite {
code: string;
@@ -465,54 +472,63 @@ describe('record', function (this: ISuite) {
it('captures mutations on adopted stylesheets', async () => {
await ctx.page.evaluate(() => {
document.body.innerHTML = `
return new Promise((resolve) => {
document.body.innerHTML = `
<div>div in outermost document</div>
<iframe></iframe>
`;
const sheet = new CSSStyleSheet();
// Add stylesheet to a document.
const sheet = new CSSStyleSheet();
// Add stylesheet to a document.
document.adoptedStyleSheets = [sheet];
document.adoptedStyleSheets = [sheet];
const iframe = document.querySelector('iframe');
const sheet2 = new (iframe!.contentWindow! as Window &
typeof globalThis).CSSStyleSheet();
const iframe = document.querySelector('iframe');
const sheet2 = new (iframe!.contentWindow! as Window &
typeof globalThis).CSSStyleSheet();
// Add stylesheet to an IFrame document.
iframe!.contentDocument!.adoptedStyleSheets = [sheet2];
iframe!.contentDocument!.body.innerHTML = '<h1>h1 in iframe</h1>';
// Add stylesheet to an IFrame document.
iframe!.contentDocument!.adoptedStyleSheets = [sheet2];
iframe!.contentDocument!.body.innerHTML = '<h1>h1 in iframe</h1>';
const { record } = ((window as unknown) as IWindow).rrweb;
record({
emit: ((window as unknown) as IWindow).emit,
const { record } = ((window as unknown) as IWindow).rrweb;
record({
emit: ((window as unknown) as IWindow).emit,
});
setTimeout(() => {
sheet.replace!('div { color: yellow; }');
sheet2.replace!('h1 { color: blue; }');
}, 0);
setTimeout(() => {
sheet.replaceSync!('div { display: inline ; }');
sheet2.replaceSync!('h1 { font-size: large; }');
}, 5);
setTimeout(() => {
(sheet.cssRules[0] as CSSStyleRule).style.setProperty(
'color',
'green',
);
(sheet.cssRules[0] as CSSStyleRule).style.removeProperty('display');
(sheet2.cssRules[0] as CSSStyleRule).style.setProperty(
'font-size',
'medium',
'important',
);
sheet2.insertRule('h2 { color: red; }');
}, 10);
setTimeout(() => {
sheet.insertRule('body { border: 2px solid blue; }', 1);
sheet2.deleteRule(0);
}, 15);
setTimeout(() => {
resolve(undefined);
}, 20);
});
setTimeout(() => {
sheet.replace!('div { color: yellow; }');
sheet2.replace!('h1 { color: blue; }');
}, 0);
setTimeout(() => {
sheet.replaceSync!('div { display: inline ; }');
sheet2.replaceSync!('h1 { font-size: large; }');
}, 5);
setTimeout(() => {
(sheet.cssRules[0] as CSSStyleRule).style.setProperty('color', 'green');
(sheet.cssRules[0] as CSSStyleRule).style.removeProperty('display');
(sheet2.cssRules[0] as CSSStyleRule).style.setProperty(
'font-size',
'medium',
'important',
);
sheet2.insertRule('h2 { color: red; }');
}, 10);
setTimeout(() => {
sheet.insertRule('body { border: 2px solid blue; }', 1);
sheet2.deleteRule(0);
}, 15);
});
await waitForRAF(ctx.page);
assertSnapshot(ctx.events);
@@ -602,70 +618,91 @@ describe('record', function (this: ISuite) {
assertSnapshot(ctx.events);
});
it('captures stylesheets that are still loading', async () => {
await ctx.page.evaluate(() => {
const { record } = ((window as unknown) as IWindow).rrweb;
describe('loading stylesheets', () => {
let server: Server;
let serverURL: string;
record({
inlineStylesheet: true,
emit: ((window as unknown) as IWindow).emit,
});
const link1 = document.createElement('link');
link1.setAttribute('rel', 'stylesheet');
link1.setAttribute(
'href',
URL.createObjectURL(
new Blob(['body { color: pink; }'], {
type: 'text/css',
}),
),
);
document.head.appendChild(link1);
beforeAll(async () => {
server = await startServer();
serverURL = getServerURL(server);
});
// `blob:` URLs are not available immediately, so we need to wait for the browser to load them
await waitForRAF(ctx.page);
// 'blob' URL is different in every execution so we need to remove it from the snapshot.
const filteredEvents = JSON.parse(
JSON.stringify(ctx.events).replace(/blob\:[\w\d-/]+"/, 'blob:null"'),
);
assertSnapshot(filteredEvents);
});
it('captures stylesheets in iframes that are still loading', async () => {
await ctx.page.evaluate(() => {
const iframe = document.createElement('iframe');
iframe.setAttribute('src', 'about:blank');
document.body.appendChild(iframe);
const iframeDoc = iframe.contentDocument!;
const { record } = ((window as unknown) as IWindow).rrweb;
record({
inlineStylesheet: true,
emit: ((window as unknown) as IWindow).emit,
beforeEach(async () => {
ctx.page = await ctx.browser.newPage();
await ctx.page.goto(`${serverURL}/html/hello-world.html`);
await ctx.page.evaluate(ctx.code);
ctx.events = [];
await ctx.page.exposeFunction('emit', (e: eventWithTime) => {
if (
e.type === EventType.DomContentLoaded ||
e.type === EventType.Load
) {
return;
}
ctx.events.push(e);
});
const linkEl = document.createElement('link');
linkEl.setAttribute('rel', 'stylesheet');
linkEl.setAttribute(
'href',
URL.createObjectURL(
new Blob(['body { color: pink; }'], {
type: 'text/css',
}),
),
);
iframeDoc.head.appendChild(linkEl);
ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
});
// `blob:` URLs are not available immediately, so we need to wait for the browser to load them
await waitForRAF(ctx.page);
const filteredEvents = JSON.parse(
JSON.stringify(ctx.events).replace(/blob\:[\w\d-/]+"/, 'blob:null"'),
);
assertSnapshot(filteredEvents);
afterAll(async () => {
await server.close();
});
it('captures stylesheets that are still loading', async () => {
ctx.page.evaluate((serverURL) => {
const { record } = ((window as unknown) as IWindow).rrweb;
record({
inlineStylesheet: true,
emit: ((window as unknown) as IWindow).emit,
});
const link1 = document.createElement('link');
link1.setAttribute('rel', 'stylesheet');
link1.setAttribute('href', `${serverURL}/html/assets/style.css`);
document.head.appendChild(link1);
}, serverURL);
await ctx.page.waitForResponse(`${serverURL}/html/assets/style.css`);
await waitForRAF(ctx.page);
assertSnapshot(ctx.events);
});
it('captures stylesheets in iframes that are still loading', async () => {
ctx.page.evaluate(() => {
const iframe = document.createElement('iframe');
iframe.setAttribute('src', `/html/hello-world.html?2`);
document.body.appendChild(iframe);
const { record } = ((window as unknown) as IWindow).rrweb;
record({
inlineStylesheet: true,
emit: ((window as unknown) as IWindow).emit,
});
});
await ctx.page.waitForResponse(`${serverURL}/html/hello-world.html?2`);
await waitForRAF(ctx.page);
ctx.page.evaluate(() => {
const iframe = document.querySelector('iframe')!;
const iframeDoc = iframe.contentDocument!;
const linkEl = document.createElement('link');
linkEl.setAttribute('rel', 'stylesheet');
linkEl.setAttribute('href', `/html/assets/style.css`);
iframeDoc.head.appendChild(linkEl);
});
await ctx.page.waitForResponse(`${serverURL}/html/assets/style.css`);
await waitForRAF(ctx.page);
assertSnapshot(ctx.events);
});
});
it('captures CORS stylesheets that are still loading', async () => {
@@ -695,65 +732,71 @@ describe('record', function (this: ISuite) {
it('captures adopted stylesheets in shadow doms and iframe', async () => {
await ctx.page.evaluate(() => {
document.body.innerHTML = `
return new Promise((resolve) => {
document.body.innerHTML = `
<div>div in outermost document</div>
<div id="shadow-host1"></div>
<div id="shadow-host2"></div>
<iframe></iframe>
`;
const sheet = new CSSStyleSheet();
sheet.replaceSync!(
'div { color: yellow; } h2 { color: orange; } h3 { font-size: larger;}',
);
// Add stylesheet to a document.
const sheet = new CSSStyleSheet();
sheet.replaceSync!(
'div { color: yellow; } h2 { color: orange; } h3 { font-size: larger;}',
);
// Add stylesheet to a document.
document.adoptedStyleSheets = [sheet];
document.adoptedStyleSheets = [sheet];
// Add stylesheet to a shadow host.
const host = document.querySelector('#shadow-host1');
const shadow = host!.attachShadow({ mode: 'open' });
shadow.innerHTML =
'<div>div in shadow dom 1</div><span>span in shadow dom 1</span>';
const sheet2 = new CSSStyleSheet();
sheet2.replaceSync!('span { color: red; }');
shadow.adoptedStyleSheets = [sheet, sheet2];
// Add stylesheet to an IFrame document.
const iframe = document.querySelector('iframe');
const sheet3 = new (iframe!.contentWindow! as IWindow &
typeof globalThis).CSSStyleSheet();
sheet3.replaceSync!('h1 { color: blue; }');
iframe!.contentDocument!.adoptedStyleSheets = [sheet3];
const ele = iframe!.contentDocument!.createElement('h1');
ele.innerText = 'h1 in iframe';
iframe!.contentDocument!.body.appendChild(ele);
((window as unknown) as IWindow).rrweb.record({
emit: ((window.top as unknown) as IWindow).emit,
});
// Make incremental changes to shadow dom.
setTimeout(() => {
const host = document.querySelector('#shadow-host2');
// Add stylesheet to a shadow host.
const host = document.querySelector('#shadow-host1');
const shadow = host!.attachShadow({ mode: 'open' });
shadow.innerHTML =
'<div>div in shadow dom 2</div><span>span in shadow dom 2</span>';
const sheet4 = new CSSStyleSheet();
sheet4.replaceSync!('span { color: green; }');
shadow.adoptedStyleSheets = [sheet, sheet4];
'<div>div in shadow dom 1</div><span>span in shadow dom 1</span>';
const sheet2 = new CSSStyleSheet();
document.adoptedStyleSheets = [sheet4, sheet, sheet2];
sheet2.replaceSync!('span { color: red; }');
const sheet5 = new (iframe!.contentWindow! as IWindow &
shadow.adoptedStyleSheets = [sheet, sheet2];
// Add stylesheet to an IFrame document.
const iframe = document.querySelector('iframe');
const sheet3 = new (iframe!.contentWindow! as IWindow &
typeof globalThis).CSSStyleSheet();
sheet5.replaceSync!('h2 { color: purple; }');
iframe!.contentDocument!.adoptedStyleSheets = [sheet5, sheet3];
}, 10);
sheet3.replaceSync!('h1 { color: blue; }');
iframe!.contentDocument!.adoptedStyleSheets = [sheet3];
const ele = iframe!.contentDocument!.createElement('h1');
ele.innerText = 'h1 in iframe';
iframe!.contentDocument!.body.appendChild(ele);
((window as unknown) as IWindow).rrweb.record({
emit: ((window.top as unknown) as IWindow).emit,
});
// Make incremental changes to shadow dom.
setTimeout(() => {
const host = document.querySelector('#shadow-host2');
const shadow = host!.attachShadow({ mode: 'open' });
shadow.innerHTML =
'<div>div in shadow dom 2</div><span>span in shadow dom 2</span>';
const sheet4 = new CSSStyleSheet();
sheet4.replaceSync!('span { color: green; }');
shadow.adoptedStyleSheets = [sheet, sheet4];
document.adoptedStyleSheets = [sheet4, sheet, sheet2];
const sheet5 = new (iframe!.contentWindow! as IWindow &
typeof globalThis).CSSStyleSheet();
sheet5.replaceSync!('h2 { color: purple; }');
iframe!.contentDocument!.adoptedStyleSheets = [sheet5, sheet3];
}, 10);
setTimeout(() => {
resolve(null);
}, 20);
});
});
await waitForRAF(ctx.page); // wait till events get sent

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,514 @@
import * as fs from 'fs';
import * as path from 'path';
import type * as puppeteer from 'puppeteer';
import type { recordOptions } from '../../src/types';
import type {
listenerHandler,
eventWithTime,
mutationData,
} from '@rrweb/types';
import { EventType, IncrementalSource } from '@rrweb/types';
import {
assertSnapshot,
getServerURL,
launchPuppeteer,
startServer,
stripBase64,
waitForRAF,
} from '../utils';
import type * as http from 'http';
interface ISuite {
code: string;
browser: puppeteer.Browser;
page: puppeteer.Page;
events: eventWithTime[];
server: http.Server;
serverURL: string;
}
interface IWindow extends Window {
rrweb: {
record: (
options: recordOptions<eventWithTime>,
) => listenerHandler | undefined;
addCustomEvent<T>(tag: string, payload: T): void;
};
emit: (e: eventWithTime) => undefined;
snapshots: eventWithTime[];
}
async function injectRecordScript(frame: puppeteer.Frame) {
await frame.addScriptTag({
path: path.resolve(__dirname, '../../dist/rrweb.js'),
});
await frame.evaluate(() => {
((window as unknown) as IWindow).snapshots = [];
const { record } = ((window as unknown) as IWindow).rrweb;
record({
recordCrossOriginIframes: true,
recordCanvas: true,
emit(event) {
((window as unknown) as IWindow).snapshots.push(event);
((window as unknown) as IWindow).emit(event);
},
});
});
for (const child of frame.childFrames()) {
await injectRecordScript(child);
}
}
const setup = function (this: ISuite, content: string): ISuite {
const ctx = {} as ISuite;
beforeAll(async () => {
ctx.browser = await launchPuppeteer();
ctx.server = await startServer();
ctx.serverURL = getServerURL(ctx.server);
// ctx.serverB = await startServer();
// ctx.serverBURL = getServerURL(ctx.serverB);
const bundlePath = path.resolve(__dirname, '../../dist/rrweb.js');
ctx.code = fs.readFileSync(bundlePath, 'utf8');
});
beforeEach(async () => {
ctx.page = await ctx.browser.newPage();
await ctx.page.goto('about:blank');
await ctx.page.setContent(
content.replace(/\{SERVER_URL\}/g, ctx.serverURL),
);
// await ctx.page.evaluate(ctx.code);
ctx.events = [];
await ctx.page.exposeFunction('emit', (e: eventWithTime) => {
if (e.type === EventType.DomContentLoaded || e.type === EventType.Load) {
return;
}
ctx.events.push(e);
});
ctx.page.on('console', (msg) => console.log('PAGE LOG:', msg.text()));
await injectRecordScript(ctx.page.mainFrame());
});
afterEach(async () => {
await ctx.page.close();
});
afterAll(async () => {
await ctx.browser.close();
ctx.server.close();
// ctx.serverB.close();
});
return ctx;
};
describe('cross origin iframes', function (this: ISuite) {
jest.setTimeout(100_000);
describe('form.html', function (this: ISuite) {
const ctx: ISuite = setup.call(
this,
`
<!DOCTYPE html>
<html>
<body>
<iframe src="{SERVER_URL}/html/form.html" style="width: 400px; height: 400px;"></iframe>
</body>
</html>
`,
);
it("won't emit events if it isn't in the top level iframe", async () => {
const el = (await ctx.page.$(
'body > iframe',
)) as puppeteer.ElementHandle<Element>;
const frame = await el.contentFrame();
const events = await frame?.evaluate(
() => ((window as unknown) as IWindow).snapshots,
);
expect(events).toMatchObject([]);
});
it('will emit events if it is in the top level iframe', async () => {
const events = await ctx.page.evaluate(
() => ((window as unknown) as IWindow).snapshots,
);
expect(events.length).not.toBe(0);
});
it('should emit contents of iframe', async () => {
const events = await ctx.page.evaluate(
() => ((window as unknown) as IWindow).snapshots,
);
await waitForRAF(ctx.page);
// two events (full snapshot + meta) from main frame, and one full snapshot from iframe
expect(events.length).toBe(3);
});
it('should emit full snapshot event from iframe as mutation event', async () => {
const events = await ctx.page.evaluate(
() => ((window as unknown) as IWindow).snapshots,
);
await waitForRAF(ctx.page);
// two events from main frame, and two from iframe
expect(events[events.length - 1]).toMatchObject({
type: EventType.IncrementalSnapshot,
data: {
source: IncrementalSource.Mutation,
adds: [
{
parentId: expect.any(Number),
node: {
id: expect.any(Number),
},
},
],
},
});
});
it('should use unique id for child of iframes', async () => {
const events: eventWithTime[] = await ctx.page.evaluate(
() => ((window as unknown) as IWindow).snapshots,
);
await waitForRAF(ctx.page);
expect(
(events[events.length - 1].data as mutationData).adds[0].node.id,
).not.toBe(1);
});
it('should replace the existing DOM nodes on iframe navigation with `isAttachIframe`', async () => {
await ctx.page.evaluate((url) => {
const iframe = document.querySelector('iframe') as HTMLIFrameElement;
iframe.src = `${url}/html/form.html?2`;
}, ctx.serverURL);
await waitForRAF(ctx.page); // loads iframe
await injectRecordScript(ctx.page.mainFrame().childFrames()[0]); // injects script into new iframe
const events: eventWithTime[] = await ctx.page.evaluate(
() => ((window as unknown) as IWindow).snapshots,
);
expect(
(events[events.length - 1].data as mutationData).removes,
).toMatchObject([]);
expect(
(events[events.length - 1].data as mutationData).isAttachIframe,
).toBeTruthy();
});
it('should map input events correctly', async () => {
const frame = ctx.page.mainFrame().childFrames()[0];
await frame.type('input[type="text"]', 'test');
await frame.click('input[type="radio"]');
await frame.click('input[type="checkbox"]');
await frame.type('input[type="password"]', 'password');
await frame.type('textarea', 'textarea test');
await frame.select('select', '1');
const snapshots = (await ctx.page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
it('should map scroll events correctly', async () => {
// force scrollbars in iframe
ctx.page.evaluate(() => {
const iframe = document.querySelector('iframe') as HTMLIFrameElement;
iframe.style.width = '50px';
iframe.style.height = '50px';
});
await waitForRAF(ctx.page);
const frame = ctx.page.mainFrame().childFrames()[0];
// scroll a little
frame.evaluate(() => {
window.scrollTo(0, 10);
});
await waitForRAF(ctx.page);
const snapshots = (await ctx.page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
});
describe('move-node.html', function (this: ISuite) {
const ctx: ISuite = setup.call(
this,
`
<!DOCTYPE html>
<html>
<body>
<iframe src="{SERVER_URL}/html/move-node.html"></iframe>
</body>
</html>
`,
);
it('should record DOM node movement', async () => {
const frame = ctx.page.mainFrame().childFrames()[0];
await frame.evaluate(() => {
const div = document.createElement('div');
const span = document.querySelector('span')!;
document.body.appendChild(div);
div.appendChild(span);
});
const snapshots = (await ctx.page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
it('should record DOM node removal', async () => {
const frame = ctx.page.mainFrame().childFrames()[0];
await frame.evaluate(() => {
const span = document.querySelector('span')!;
span.remove();
});
const snapshots = (await ctx.page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
it('should record DOM attribute changes', async () => {
const frame = ctx.page.mainFrame().childFrames()[0];
await frame.evaluate(() => {
const span = document.querySelector('span')!;
span.className = 'added-class-name';
});
const snapshots = (await ctx.page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
it('should record DOM text changes', async () => {
const frame = ctx.page.mainFrame().childFrames()[0];
await frame.evaluate(() => {
const b = document.querySelector('b')!;
b.childNodes[0].textContent = 'replaced text';
});
const snapshots = (await ctx.page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
it('should record canvas elements', async () => {
const frame = ctx.page.mainFrame().childFrames()[0];
await frame.evaluate(() => {
var canvas = document.createElement('canvas');
var gl = canvas.getContext('webgl')!;
var program = gl.createProgram()!;
gl.linkProgram(program);
gl.clear(gl.COLOR_BUFFER_BIT);
document.body.appendChild(canvas);
});
await waitForRAF(ctx.page);
const snapshots = (await ctx.page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
it('should record custom events', async () => {
const frame = ctx.page.mainFrame().childFrames()[0];
await frame.evaluate(() => {
((window as unknown) as IWindow).rrweb.addCustomEvent('test', {
id: 1,
parentId: 1,
nextId: 2,
});
});
await waitForRAF(ctx.page);
const snapshots = (await ctx.page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
it('captures mutations on adopted stylesheets', async () => {
const frame = ctx.page.mainFrame().childFrames()[0];
await ctx.page.evaluate(() => {
const sheet = new CSSStyleSheet();
// Add stylesheet to a document.
document.adoptedStyleSheets = [sheet];
});
await frame.evaluate(() => {
const sheet = new CSSStyleSheet();
// Add stylesheet to a document.
document.adoptedStyleSheets = [sheet];
});
await waitForRAF(ctx.page);
await ctx.page.evaluate(() => {
document.adoptedStyleSheets![0].replace!('div { color: yellow; }');
});
await frame.evaluate(() => {
document.adoptedStyleSheets![0].replace!('h1 { color: blue; }');
});
await waitForRAF(ctx.page);
await ctx.page.evaluate(() => {
document.adoptedStyleSheets![0].replaceSync!(
'div { display: inline ; }',
);
});
await frame.evaluate(() => {
document.adoptedStyleSheets![0].replaceSync!(
'h1 { font-size: large; }',
);
});
await waitForRAF(ctx.page);
await ctx.page.evaluate(() => {
(document.adoptedStyleSheets![0]
.cssRules[0] as CSSStyleRule).style.setProperty('color', 'green');
(document.adoptedStyleSheets![0]
.cssRules[0] as CSSStyleRule).style.removeProperty('display');
});
await frame.evaluate(() => {
(document.adoptedStyleSheets![0]
.cssRules[0] as CSSStyleRule).style.setProperty(
'font-size',
'medium',
'important',
);
document.adoptedStyleSheets![0].insertRule('h2 { color: red; }');
});
await waitForRAF(ctx.page);
await ctx.page.evaluate(() => {
document.adoptedStyleSheets![0].insertRule(
'body { border: 2px solid blue; }',
1,
);
});
await frame.evaluate(() => {
document.adoptedStyleSheets![0].deleteRule(0);
});
await waitForRAF(ctx.page);
const snapshots = (await ctx.page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
it('captures mutations on stylesheets', async () => {
const frame = ctx.page.mainFrame().childFrames()[0];
await ctx.page.evaluate(() => {
// Add stylesheet to a document.
const style = document.createElement('style');
document.head.appendChild(style);
});
await frame.evaluate(() => {
// Add stylesheet to a document.
const style = document.createElement('style');
document.head.appendChild(style);
});
await waitForRAF(ctx.page);
await ctx.page.evaluate(() => {
document.styleSheets[0].insertRule('div { color: yellow; }');
});
await frame.evaluate(() => {
document.styleSheets[0].insertRule('h1 { color: blue; }');
});
await waitForRAF(ctx.page);
await ctx.page.evaluate(() => {
(document.styleSheets[0].cssRules[0] as CSSStyleRule).style.setProperty(
'color',
'green',
);
(document.styleSheets[0]
.cssRules[0] as CSSStyleRule).style.removeProperty('display');
});
await frame.evaluate(() => {
(document.styleSheets[0].cssRules[0] as CSSStyleRule).style.setProperty(
'font-size',
'medium',
'important',
);
document.styleSheets[0].insertRule('h2 { color: red; }');
});
await waitForRAF(ctx.page);
await ctx.page.evaluate(() => {
document.styleSheets[0].insertRule(
'body { border: 2px solid blue; }',
1,
);
});
await frame.evaluate(() => {
document.styleSheets[0].deleteRule(0);
});
await waitForRAF(ctx.page);
const snapshots = (await ctx.page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
});
describe('audio.html', function (this: ISuite) {
jest.setTimeout(100_000);
const ctx: ISuite = setup.call(
this,
`
<!DOCTYPE html>
<html>
<body>
<iframe src="{SERVER_URL}/html/audio.html"></iframe>
</body>
</html>
`,
);
it('should emit contents of iframe once', async () => {
const frame = ctx.page.mainFrame().childFrames()[0];
await frame.evaluate(() => {
const el = document.querySelector('audio')!;
el.play();
});
await waitForRAF(ctx.page);
const snapshots = (await ctx.page.evaluate(
'window.snapshots',
)) as eventWithTime[];
assertSnapshot(snapshots);
});
});
});
describe('same origin iframes', function (this: ISuite) {
jest.setTimeout(100_000);
const ctx: ISuite = setup.call(
this,
`
<!DOCTYPE html>
<html>
<body>
<iframe src="about:blank"></iframe>
</body>
</html>
`,
);
it('should emit contents of iframe once', async () => {
const events = await ctx.page.evaluate(
() => ((window as unknown) as IWindow).snapshots,
);
await waitForRAF(ctx.page);
// two events (full snapshot + meta) from main frame,
// and two (full snapshot + mutation) from iframe
expect(events.length).toBe(4);
assertSnapshot(events);
});
});

View File

@@ -191,6 +191,11 @@ function stringifySnapshots(snapshots: eventWithTime[]): string {
}),
null,
2,
).replace(
// servers might get run on a random port,
// so we need to normalize the port number
/http:\/\/localhost:\d+/g,
'http://localhost:3030',
);
}