Skip to content

Commit 521e76c

Browse files
committed
Fix click and fill value order in some cases... Improve canvas events
1 parent 7fdecae commit 521e76c

9 files changed

Lines changed: 208 additions & 101 deletions

File tree

src/pages/Content/modules/collectEvents.js

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,59 @@ const lastNodes = {
1919
};
2020

2121
let stoMouseover = false;
22+
let pendingInput = null;
23+
let flushedInput = null;
24+
const keystrokeBuffers = new WeakMap();
25+
26+
function getEffectiveValue(element) {
27+
const domValue = element.value;
28+
if (domValue && domValue.trim()) {
29+
return domValue;
30+
}
31+
return keystrokeBuffers.get(element) || domValue;
32+
}
33+
34+
function isBufferedValue(element) {
35+
const domValue = element.value;
36+
return !(domValue && domValue.trim()) && keystrokeBuffers.has(element);
37+
}
38+
39+
function isCanvasSurface(element) {
40+
if (element.nodeName.toLowerCase() === 'canvas') return true;
41+
if (element.shadowRoot) {
42+
try {
43+
if (element.shadowRoot.querySelector('canvas')) return true;
44+
} catch (ex) { /* ignore */ }
45+
}
46+
for (const child of element.children || []) {
47+
if (child.shadowRoot) {
48+
try {
49+
if (child.shadowRoot.querySelector('canvas')) return true;
50+
} catch (ex) { /* ignore */ }
51+
}
52+
}
53+
return false;
54+
}
55+
56+
function getStableInputSelector(element, doc) {
57+
const dynamicPatterns = ['focus', 'highlight', 'editable', 'caret'];
58+
const classes = Array.from(element.classList || []).filter((cls) => {
59+
const lower = cls.toLowerCase();
60+
return !dynamicPatterns.some((p) => lower.includes(p)) && !/^[0-9]/.test(cls);
61+
});
62+
for (const cls of classes) {
63+
const sel = '.' + cls;
64+
try {
65+
const matches = doc.querySelectorAll(sel);
66+
if (matches.length === 1 && matches[0] === element) {
67+
return sel;
68+
}
69+
} catch (ex) {
70+
// ignore
71+
}
72+
}
73+
return null;
74+
}
2275

2376
export function interceptEvents(event, doc, ifrSelector, callback) {
2477
let hasKeyReturn = false;
@@ -110,6 +163,26 @@ export function interceptEvents(event, doc, ifrSelector, callback) {
110163
if (type === 'click') {
111164
lastNodes.click = tgt;
112165

166+
if (pendingInput && pendingInput.element !== tgt && callback) {
167+
const useBuffer = isBufferedValue(pendingInput.element);
168+
const fillEvent = {
169+
...pendingInput.oEventBase,
170+
type: useBuffer ? 'bfill_value' : 'fill_value',
171+
value: getEffectiveValue(pendingInput.element),
172+
frame: pendingInput.frame,
173+
};
174+
if (useBuffer) {
175+
const stableSel = getStableInputSelector(pendingInput.element, doc);
176+
if (stableSel) {
177+
fillEvent.css = stableSel;
178+
}
179+
}
180+
callback({ messageType: 'events', event: { ...fillEvent } });
181+
keystrokeBuffers.delete(pendingInput.element);
182+
flushedInput = pendingInput.element;
183+
pendingInput = null;
184+
}
185+
113186
if (
114187
lastNodes.return === lastNodes.change &&
115188
tgt !== lastNodes.return &&
@@ -134,7 +207,7 @@ export function interceptEvents(event, doc, ifrSelector, callback) {
134207
}
135208
}
136209
let typeStr = 'click';
137-
if (nodeName === 'canvas') {
210+
if (isCanvasSurface(tgt)) {
138211
const rect = tgt.getBoundingClientRect();
139212
const x = event.clientX - rect.left;
140213
const y = event.clientY - rect.top;
@@ -261,6 +334,16 @@ export function interceptEvents(event, doc, ifrSelector, callback) {
261334
lastNodes.keydown = tgt;
262335
if (['input', 'textarea'].indexOf(nodeName) > -1) {
263336
oNodes[tgt] = tgt.value;
337+
pendingInput = { element: tgt, oEventBase: { ...oEventBase }, frame: ifrSelector };
338+
if (event.key && event.key.length === 1) {
339+
const buf = keystrokeBuffers.get(tgt) || '';
340+
keystrokeBuffers.set(tgt, buf + event.key);
341+
} else if (event.key === 'Backspace') {
342+
const buf = keystrokeBuffers.get(tgt) || '';
343+
if (buf.length > 0) {
344+
keystrokeBuffers.set(tgt, buf.slice(0, -1));
345+
}
346+
}
264347
}
265348
if (
266349
nodeName === 'input' &&
@@ -271,15 +354,30 @@ export function interceptEvents(event, doc, ifrSelector, callback) {
271354
hasKeyReturn = true;
272355
// lastSelectorWithReturn = selector;
273356
lastNodes.return = tgt;
357+
const useBuffer = isBufferedValue(tgt);
274358
oEventToSend = {
275359
...oEventBase,
276-
type: 'fill_value',
277-
value: tgt.value,
360+
type: useBuffer ? 'bfill_value' : 'fill_value',
361+
value: getEffectiveValue(tgt),
278362
frame: ifrSelector,
279363
};
364+
if (useBuffer) {
365+
const stableSel = getStableInputSelector(tgt, doc);
366+
if (stableSel) {
367+
oEventToSend.css = stableSel;
368+
}
369+
}
370+
keystrokeBuffers.delete(tgt);
280371
}
281372
} else if (type === 'blur') {
282373
lastNodes.blur = tgt;
374+
if (tgt === flushedInput) {
375+
flushedInput = null;
376+
return;
377+
}
378+
if (pendingInput && pendingInput.element === tgt) {
379+
pendingInput = null;
380+
}
283381
if (['input', 'textarea'].indexOf(nodeName) > -1) {
284382
oNodes[tgt] = tgt.value;
285383
if (tgt === lastNodes.return) {
@@ -292,12 +390,20 @@ export function interceptEvents(event, doc, ifrSelector, callback) {
292390
) {
293391
return;
294392
}
393+
const useBuffer = isBufferedValue(tgt);
295394
oEventToSend = {
296395
...oEventBase,
297-
type: 'fill_value',
298-
value: tgt.value,
396+
type: useBuffer ? 'bfill_value' : 'fill_value',
397+
value: getEffectiveValue(tgt),
299398
frame: ifrSelector,
300399
};
400+
if (useBuffer) {
401+
const stableSel = getStableInputSelector(tgt, doc);
402+
if (stableSel) {
403+
oEventToSend.css = stableSel;
404+
}
405+
}
406+
keystrokeBuffers.delete(tgt);
301407
}
302408
} else if (type === 'change') {
303409
lastNodes.change = tgt;

src/pages/Popup/Popup.jsx

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,30 @@ import logo from '../../assets/img/logo_probely.svg';
33
import help from '../../assets/img/help.svg';
44
import './Popup.css';
55

6-
const helpURL = 'https://help.probely.com/en/articles/5402869-how-to-record-a-sequence-with-probely-s-sequence-recorder-plugin';
6+
const helpURL = 'https://docs.snyk.io/scan-fix-and-prevent/scan-with-snyk/snyk-api-web/configure-targets/configure-web-targets/use-sequence-recorder';
77

88
const Popup = (props) => {
99
// 🔴
1010
// console.log('PROPS :: ', props);
1111
const [isRecording, setIsRecording] = useState(false);
1212
const [startURL, setStartURL] = useState('');
1313
const [recordingData, setRecordingData] = useState([]);
14-
const [copyStatus, setCopyStatus] = useState({status: false, error: false, msg: 'Successfully copied to clipboard'});
14+
const [copyStatus, setCopyStatus] = useState({ status: false, error: false, msg: 'Successfully copied to clipboard' });
1515

1616
useEffect(() => {
1717
if (chrome && chrome.storage) {
1818
chrome.storage.sync.get(['isRecording'], (data) => {
1919
const recording = data.isRecording;
20-
if(recording) {
20+
if (recording) {
2121
setIsRecording(true);
2222
(chrome.action || chrome.browserAction).setBadgeText({
2323
text: '🔴',
24-
}, () => {});
24+
}, () => { });
2525
} else {
2626
setIsRecording(false);
2727
(chrome.action || chrome.browserAction).setBadgeText({
2828
text: '',
29-
}, () => {});
29+
}, () => { });
3030
}
3131
});
3232
chrome.runtime.onMessage.addListener((data, sender, sendResponse) => {
@@ -51,8 +51,8 @@ const Popup = (props) => {
5151
if (chrome) {
5252
(chrome.action || chrome.browserAction).setBadgeText({
5353
text: '🔴',
54-
}, () => {});
55-
chrome.storage.sync.set({isRecording: true}, () => {
54+
}, () => { });
55+
chrome.storage.sync.set({ isRecording: true }, () => {
5656
chrome.runtime.sendMessage({
5757
messageType: 'start',
5858
event: {
@@ -63,7 +63,7 @@ const Popup = (props) => {
6363
url: startURL,
6464
},
6565
});
66-
chrome.tabs.create({active: true, url: startURL}, (aa) => {
66+
chrome.tabs.create({ active: true, url: startURL }, (aa) => {
6767
});
6868
});
6969
}
@@ -74,15 +74,15 @@ const Popup = (props) => {
7474
if (chrome) {
7575
(chrome.action || chrome.browserAction).setBadgeText({
7676
text: '',
77-
}, () => {});
78-
chrome.storage.sync.set({isRecording: false}, () => {
77+
}, () => { });
78+
chrome.storage.sync.set({ isRecording: false }, () => {
7979
askForRecordingData();
8080

81-
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
81+
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
8282
if (tabs && tabs.length) {
8383
const curTab = tabs[0];
8484
chrome.tabs.remove(curTab.id);
85-
chrome.tabs.create({active: true, url: './review.html'}, (aa) => {
85+
chrome.tabs.create({ active: true, url: './review.html' }, (aa) => {
8686
});
8787
}
8888
});
@@ -126,7 +126,7 @@ const Popup = (props) => {
126126
msg: 'Successfully copied to clipboard'
127127
});
128128
setTimeout(() => {
129-
setCopyStatus({status: false, error: false, msg: ''});
129+
setCopyStatus({ status: false, error: false, msg: '' });
130130
}, 3000);
131131
} else {
132132
setCopyStatus({
@@ -135,7 +135,7 @@ const Popup = (props) => {
135135
msg: 'Error on copy to clipboard'
136136
});
137137
setTimeout(() => {
138-
setCopyStatus({status: false, error: false, msg: ''});
138+
setCopyStatus({ status: false, error: false, msg: '' });
139139
}, 5000);
140140
}
141141
}
@@ -144,7 +144,7 @@ const Popup = (props) => {
144144
function onClickDownload() {
145145
var blob = new Blob([JSON.stringify(recordingData, null, 2)], {
146146
type: "text/plain;charset=utf-8"
147-
});
147+
});
148148
var a = document.createElement('a');
149149
a.download = 'snyk-api-and-web-recording.json';
150150
a.rel = 'noopener';
@@ -169,7 +169,7 @@ const Popup = (props) => {
169169

170170
function onClickHelpLink(ev) {
171171
ev.preventDefault();
172-
chrome.tabs.create({active: true, url: helpURL}, (aa) => {
172+
chrome.tabs.create({ active: true, url: helpURL }, (aa) => {
173173
});
174174
}
175175

@@ -181,8 +181,8 @@ const Popup = (props) => {
181181
</header>
182182
<div className="App-container">
183183
<p>
184-
Use this plugin to record a sequence of steps to be followed by Snyk API & Web during a scan.{' '}
185-
When you finish recording, upload the script to your target settings.
184+
Use this plugin to record a sequence of steps to be followed by Snyk API & Web during a scan.{' '}
185+
When you finish recording, upload the script to your target settings.
186186
</p>
187187
<p className="help-container">
188188
<a
@@ -202,7 +202,7 @@ const Popup = (props) => {
202202
<div className="input-url-container">
203203
{isRecording ?
204204
null
205-
:
205+
:
206206
<>
207207
<label className="start_url_label" htmlFor="start_url">Type the start URL to be recorded</label>
208208
<input
@@ -221,14 +221,14 @@ const Popup = (props) => {
221221
}
222222
</div>
223223
<div className="buttons-container">
224-
{isRecording ?
224+
{isRecording ?
225225
// eslint-disable-next-line jsx-a11y/anchor-is-valid
226226
<p><a
227227
href="#"
228228
className="App-button"
229229
onClick={(ev) => { onClickStartStopRecording(ev, false); }}
230230
>Stop recording</a></p>
231-
:
231+
:
232232
<button
233233
type="submit"
234234
className="App-button"
@@ -260,15 +260,15 @@ const Popup = (props) => {
260260
>Clear recording data</button>
261261
</div>
262262
</>
263-
: null}
263+
: null}
264264
<div className="copy-status-container">
265265
{copyStatus.status ?
266-
<div className={copyStatus.error ? 'copy-status error' : 'copy-status success'}>{copyStatus.msg}</div>
267-
: null}
266+
<div className={copyStatus.error ? 'copy-status error' : 'copy-status success'}>{copyStatus.msg}</div>
267+
: null}
268268
</div>
269-
{recordingData.length ?
269+
{recordingData.length ?
270270
<textarea id="input-copy-to-clipboard" defaultValue={JSON.stringify(recordingData, null, 2)}></textarea>
271-
: null}
271+
: null}
272272
</div>
273273
);
274274
};

0 commit comments

Comments
 (0)