Skip to content

Commit c42c267

Browse files
authored
Merge pull request #336 from ulixee/waitForVisible
Bug fixes for SA
2 parents 21acc1d + b9e7620 commit c42c267

File tree

10 files changed

+381
-239
lines changed

10 files changed

+381
-239
lines changed

commons/Timer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default class Timer {
3838
}
3939

4040
public isExpired(): boolean {
41-
return this.elapsedMillis() > this.timeoutMillis;
41+
return this.elapsedMillis() >= this.timeoutMillis;
4242
}
4343

4444
public isResolved(): boolean {

core/injected-scripts/jsPath.ts

Lines changed: 75 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -164,42 +164,41 @@ class JsPath {
164164
): Promise<IExecJsPathResult<INodeVisibility>> {
165165
const objectAtPath = new ObjectAtPath(jsPath, containerOffset);
166166
try {
167-
return await new Promise<IExecJsPathResult<INodeVisibility>>(async resolve => {
168-
const end = new Date();
169-
end.setTime(end.getTime() + timeoutMillis);
170-
171-
while (new Date() < end) {
172-
try {
173-
if (!objectAtPath.objectAtPath) objectAtPath.lookup();
167+
const end = new Date();
168+
end.setTime(end.getTime() + (timeoutMillis || 0));
169+
170+
while (new Date() < end) {
171+
try {
172+
if (!objectAtPath.objectAtPath) objectAtPath.lookup();
173+
174+
let isElementValid = !!objectAtPath.objectAtPath;
175+
let visibility: INodeVisibility = {
176+
nodeExists: isElementValid,
177+
};
178+
if (isElementValid && waitForVisible) {
179+
visibility = objectAtPath.getComputedVisibility();
180+
isElementValid = visibility.isVisible;
181+
}
174182

175-
let isElementValid = !!objectAtPath.objectAtPath;
176-
let visibility: INodeVisibility = {
177-
nodeExists: isElementValid,
183+
if (isElementValid) {
184+
return {
185+
nodePointer: objectAtPath.extractNodePointer(),
186+
value: visibility,
178187
};
179-
if (isElementValid && waitForVisible) {
180-
visibility = objectAtPath.getComputedVisibility();
181-
isElementValid = visibility.isVisible;
182-
}
183-
184-
if (isElementValid) {
185-
return resolve({
186-
nodePointer: objectAtPath.extractNodePointer(),
187-
value: visibility,
188-
});
189-
}
190-
} catch (err) {
191-
// can happen if lookup path is bad
192188
}
193-
// eslint-disable-next-line promise/param-names
194-
await new Promise(resolve1 => setTimeout(resolve1, 20));
195-
await new Promise(requestAnimationFrame);
189+
} catch (err) {
190+
if (String(err).includes('not a valid selector')) throw err;
191+
// can also happen if lookup path doesn't exist yet... in which case we want to keep trying
196192
}
193+
// eslint-disable-next-line promise/param-names
194+
await new Promise(resolve1 => setTimeout(resolve1, 20));
195+
await new Promise(requestAnimationFrame);
196+
}
197197

198-
resolve(<IExecJsPathResult>{
199-
nodePointer: objectAtPath.extractNodePointer(),
200-
value: objectAtPath.getComputedVisibility(),
201-
});
202-
});
198+
return {
199+
nodePointer: objectAtPath.extractNodePointer(),
200+
value: objectAtPath.getComputedVisibility(),
201+
};
203202
} catch (error) {
204203
return objectAtPath.toReturnError(error);
205204
}
@@ -248,8 +247,8 @@ class ObjectAtPath {
248247
const element = this.closestElement;
249248
if (!element) return null;
250249
const { x, y, width, height } = element.getBoundingClientRect();
251-
const centerX = Math.floor(100 * x + width / 2) / 100;
252-
const centerY = Math.floor(100 * y + height / 2) / 100;
250+
const centerX = round(x + width / 2);
251+
const centerY = round(y + height / 2);
253252

254253
this._obstructedByElement = document.elementFromPoint(centerX, centerY);
255254
return this._obstructedByElement;
@@ -349,38 +348,48 @@ class ObjectAtPath {
349348
}
350349

351350
public lookup() {
352-
this.objectAtPath = window;
353-
if (this.jsPath[0] === 'window') this.jsPath.shift();
354-
this.lookupStepIndex = 0;
355-
for (const step of this.jsPath) {
356-
this.lookupStep = step;
357-
if (Array.isArray(step)) {
358-
const [methodName, ...args] = step;
359-
// extract node ids as args
360-
const finalArgs = args.map(x => {
361-
if (typeof x !== 'string') return x;
362-
if (!x.startsWith('$$jsPath=')) return x;
363-
const innerPath = JSON.parse(x.split('$$jsPath=').pop());
364-
const sub = new ObjectAtPath(innerPath, this.containerOffset).lookup();
365-
return sub.objectAtPath;
366-
});
367-
// handlers for getComputedStyle/Visibility/getNodeId/getBoundingRect
368-
if (methodName.startsWith('__') && methodName.endsWith('__')) {
369-
this.hasCustomMethodLookup = true;
370-
this.objectAtPath = this[`${methodName.replace(/__/g, '')}`](...finalArgs);
351+
try {
352+
// track object as we navigate so we can extract properties along the way
353+
this.objectAtPath = window;
354+
this.lookupStepIndex = 0;
355+
if (this.jsPath[0] === 'window') {
356+
this.jsPath.shift();
357+
this.lookupStepIndex = 1;
358+
}
359+
for (const step of this.jsPath) {
360+
this.lookupStep = step;
361+
if (Array.isArray(step)) {
362+
const [methodName, ...args] = step;
363+
// extract node ids as args
364+
const finalArgs = args.map(x => {
365+
if (typeof x !== 'string') return x;
366+
if (!x.startsWith('$$jsPath=')) return x;
367+
const innerPath = JSON.parse(x.split('$$jsPath=').pop());
368+
const sub = new ObjectAtPath(innerPath, this.containerOffset).lookup();
369+
return sub.objectAtPath;
370+
});
371+
// handlers for getComputedStyle/Visibility/getNodeId/getBoundingRect
372+
if (methodName.startsWith('__') && methodName.endsWith('__')) {
373+
this.hasCustomMethodLookup = true;
374+
this.objectAtPath = this[`${methodName.replace(/__/g, '')}`](...finalArgs);
375+
} else {
376+
const methodProperty = propertyName(methodName);
377+
this.objectAtPath = this.objectAtPath[methodProperty](...finalArgs);
378+
}
379+
} else if (typeof step === 'number') {
380+
this.objectAtPath = NodeTracker.getWatchedNodeWithId(step);
381+
} else if (typeof step === 'string') {
382+
const prop = propertyName(step);
383+
this.objectAtPath = this.objectAtPath[prop];
371384
} else {
372-
const methodProperty = propertyName(methodName);
373-
this.objectAtPath = this.objectAtPath[methodProperty](...finalArgs);
385+
throw new Error('unknown JsPathStep');
374386
}
375-
} else if (typeof step === 'number') {
376-
this.objectAtPath = NodeTracker.getWatchedNodeWithId(step);
377-
} else if (typeof step === 'string') {
378-
const prop = propertyName(step);
379-
this.objectAtPath = this.objectAtPath[prop];
380-
} else {
381-
throw new Error('unknown JsPathStep');
387+
this.lookupStepIndex += 1;
382388
}
383-
this.lookupStepIndex += 1;
389+
} catch (err) {
390+
// don't store the invalid path if we failed at a step
391+
this.objectAtPath = null;
392+
throw err;
384393
}
385394

386395
return this;
@@ -555,3 +564,7 @@ function isIterableOrArray(object) {
555564
if (!object || typeof object === 'string' || object instanceof String) return false;
556565
return !!object[Symbol.iterator] || Array.isArray(object);
557566
}
567+
568+
function round(num: number): number {
569+
return Math.floor(100 * num) / 100;
570+
}

core/lib/FrameEnvironment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ b) Use the UserProfile feature to set cookies for 1 or more domains before they'
393393
if (!waitForVisible) isValid = isNodeVisible.value?.nodeExists;
394394
if (isValid) return true;
395395
} catch (err) {
396+
if (String(err).includes('not a valid selector')) throw err;
396397
// don't log during loop
397398
}
398399

core/test/navigation.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,43 @@ setTimeout(function() {
348348

349349
it.todo('handles going to about:blank');
350350

351+
it('can wait for another tab', async () => {
352+
let userAgentString1: string;
353+
let userAgentString2: string;
354+
koaServer.get('/tabTest', ctx => {
355+
userAgentString1 = ctx.get('user-agent');
356+
ctx.body = `<body>
357+
<a target="_blank" href="/tabTestDest">Nothing really here</a>
358+
</body>`;
359+
});
360+
koaServer.get('/tabTestDest', ctx => {
361+
userAgentString2 = ctx.get('user-agent');
362+
ctx.body = `<body><h1 id="newTabHeader">You are here</h1></body>`;
363+
});
364+
const { tab } = await createSession();
365+
await tab.goto(`${koaServer.baseUrl}/tabTest`);
366+
await tab.interact([
367+
{
368+
command: InteractionCommand.click,
369+
mousePosition: ['window', 'document', ['querySelector', 'a']],
370+
},
371+
]);
372+
373+
const session = tab.session;
374+
375+
const newTab = await tab.waitForNewTab();
376+
expect(session.tabsById.size).toBe(2);
377+
await newTab.waitForLoad('PaintingStable');
378+
const header = await newTab.execJsPath([
379+
'document',
380+
['querySelector', '#newTabHeader'],
381+
'textContent',
382+
]);
383+
expect(header.value).toBe('You are here');
384+
expect(userAgentString1).toBe(userAgentString2);
385+
await newTab.close();
386+
});
387+
351388
it('should not trigger location change for first navigation of new tabs', async () => {
352389
const { tab } = await createSession();
353390

core/test/tab.test.ts

Lines changed: 0 additions & 165 deletions
This file was deleted.

0 commit comments

Comments
 (0)