Skip to content

Commit 7fdecae

Browse files
authored
Added nested iframes support and recording coordinates on canvas elements (#13)
1 parent 7e1bba0 commit 7fdecae

11 files changed

Lines changed: 153 additions & 12 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "snyk-api-and-web-record-sequence",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"description": "Snyk API & Web Record login/sequence",
55
"license": "MIT",
66
"scripts": {

src/manifest-firefox.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "Snyk API & Web Sequence Recorder",
3-
"version": "1.0.0",
3+
"version": "1.1.0",
44
"browser_specific_settings": {
55
"gecko": {
66
"id": "sequence-recorder@probely.com",

src/pages/Content/index.js

Lines changed: 91 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,21 @@ import getCustomSelector from './modules/getCustomSelector';
4949
evMsg.data.source &&
5050
evMsg.data.source === 'event-from-iframe'
5151
) {
52-
const obj = { ...evMsg.data.obj };
52+
// Verify the message actually came from a child iframe
5353
const aFrames = document.querySelectorAll('iframe, frame');
54+
let isFromChildFrame = false;
55+
for (const frame of aFrames) {
56+
if (frame.contentWindow === evMsg.source) {
57+
isFromChildFrame = true;
58+
break;
59+
}
60+
}
61+
if (!isFromChildFrame) {
62+
return; // Message didn't come from any of our iframes
63+
}
64+
65+
const obj = { ...evMsg.data.obj };
66+
const framePath = evMsg.data.framePath || [];
5467
for (const frame of aFrames) {
5568
if (frame.contentWindow === evMsg.source) {
5669
let frameSelector = null;
@@ -81,7 +94,9 @@ import getCustomSelector from './modules/getCustomSelector';
8194
}
8295
}
8396
if (frameSelector && obj.event) {
84-
obj.event.frame = frameSelector;
97+
// Build the complete frame path: [outermost, ..., innermost]
98+
const completeFramePath = [frameSelector, ...framePath];
99+
obj.event.frame = completeFramePath.join(' >>> ');
85100
chrome.runtime.sendMessage(obj);
86101
}
87102
break;
@@ -91,17 +106,90 @@ import getCustomSelector from './modules/getCustomSelector';
91106
});
92107
}
93108

109+
// intermediate frames: listen for messages from child iframes and forward to parent
110+
if (window !== window.top) {
111+
window.addEventListener('message', (evMsg) => {
112+
if (
113+
evMsg.data &&
114+
evMsg.data.source &&
115+
evMsg.data.source === 'event-from-iframe'
116+
) {
117+
// Verify the message actually came from a child iframe
118+
const aFrames = document.querySelectorAll('iframe, frame');
119+
let isFromChildFrame = false;
120+
for (const frame of aFrames) {
121+
if (frame.contentWindow === evMsg.source) {
122+
isFromChildFrame = true;
123+
break;
124+
}
125+
}
126+
if (!isFromChildFrame) {
127+
return; // Message didn't come from any of our iframes
128+
}
129+
130+
// This is an intermediate frame - find the child iframe and add to path
131+
const framePath = evMsg.data.framePath || [];
132+
133+
for (const frame of aFrames) {
134+
if (frame.contentWindow === evMsg.source) {
135+
let frameSelector = null;
136+
try {
137+
frameSelector = getCustomSelector(frame);
138+
} catch (ex) {
139+
// ignore
140+
}
141+
if (!frameSelector) {
142+
try {
143+
frameSelector = getNodeSelector(frame, {
144+
root: window.document,
145+
idName: (name) => {
146+
return !/^[0-9]+.*/i.test(name);
147+
},
148+
className: (name) => {
149+
return (
150+
!name.includes('focus') &&
151+
!name.includes('highlight') &&
152+
!/^[0-9]+.*/i.test(name)
153+
);
154+
},
155+
});
156+
} catch (ex) {
157+
// ignore
158+
}
159+
}
160+
161+
// Forward to parent with this frame's selector added to path
162+
if (frameSelector) {
163+
framePath.push(frameSelector);
164+
}
165+
166+
window.parent.postMessage(
167+
{
168+
source: 'event-from-iframe',
169+
obj: evMsg.data.obj,
170+
framePath: framePath,
171+
},
172+
'*'
173+
);
174+
break;
175+
}
176+
}
177+
}
178+
});
179+
}
180+
94181
function eventInterceptopMainHandler(ev) {
95182
if (ev && ev.type === 'mouseover' && !mutationDetected) {
96183
return;
97184
}
98185
interceptEvents(ev, window.document, null, (obj) => {
99186
if (window !== window.top) {
100-
// If the event is inside a frame, send it to the top
187+
// If the event is inside a frame, send it to the parent
101188
window.parent.postMessage(
102189
{
103190
source: 'event-from-iframe',
104191
obj: { ...obj },
192+
framePath: [], // Start with empty path, will be built as message bubbles up
105193
},
106194
'*'
107195
);

src/pages/Content/modules/collectEvents.js

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,64 @@ export function interceptEvents(event, doc, ifrSelector, callback) {
133133
return;
134134
}
135135
}
136+
let typeStr = 'click';
137+
if (nodeName === 'canvas') {
138+
const rect = tgt.getBoundingClientRect();
139+
const x = event.clientX - rect.left;
140+
const y = event.clientY - rect.top;
141+
const width = rect.width;
142+
const height = rect.height;
143+
144+
const clickData = { x, y, width, height };
145+
oEventBase = { ...oEventBase, coords: clickData };
146+
147+
typeStr = 'bclick';
148+
}
136149
oEventToSend = {
137150
...oEventBase,
138-
type: 'click',
151+
type: typeStr,
139152
value: (tgt.value || tgt.textContent || '')
140153
.trim()
141154
.substr(0, 20)
142155
.replace(/\n/gi, ''),
143156
frame: ifrSelector,
144157
};
158+
159+
// Add shadow host CSS selector for bclick events inside shadow DOM
160+
if (typeStr === 'bclick' && shadowRootIdx > -1 && composedPath) {
161+
const shadowRoot = composedPath[shadowRootIdx];
162+
const shadowHost = shadowRoot.host;
163+
if (shadowHost) {
164+
let shadowHostSelector = null;
165+
try {
166+
shadowHostSelector = getCustomSelector(shadowHost, doc);
167+
} catch (ex) {
168+
// ignore
169+
}
170+
if (!shadowHostSelector) {
171+
try {
172+
shadowHostSelector = getNodeSelector(shadowHost, {
173+
root: doc,
174+
idName: (name) => {
175+
return !/^[0-9]+.*/i.test(name);
176+
},
177+
className: (name) => {
178+
return (
179+
!name.includes('focus') &&
180+
!name.includes('highlight') &&
181+
!/^[0-9]+.*/i.test(name)
182+
);
183+
},
184+
});
185+
} catch (ex) {
186+
// ignore
187+
}
188+
}
189+
if (shadowHostSelector) {
190+
oEventToSend.shadow_host_css = shadowHostSelector;
191+
}
192+
}
193+
}
145194
} else if (type === 'dblclick') {
146195
lastNodes.dblclick = tgt;
147196
oEventToSend = {

src/pages/Review/Review.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@ const Review = (props) => {
244244
case 'goto':
245245
newType = 'go to';
246246
break;
247+
case 'click':
248+
case 'bclick':
249+
newType = 'click';
250+
break;
247251
default:
248252
newType = type;
249253
break;

0 commit comments

Comments
 (0)