Skip to content

Commit

Permalink
Merge pull request #784 from qawolf/feat-iframes
Browse files Browse the repository at this point in the history
IFrame support
  • Loading branch information
aldeed committed Aug 17, 2020
2 parents d725c37 + 88eab13 commit aae323b
Show file tree
Hide file tree
Showing 14 changed files with 266 additions and 64 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -62,7 +62,7 @@
},
"devDependencies": {
"@ffmpeg-installer/ffmpeg": "^1.0.20",
"@qawolf/sandbox": "0.1.18",
"@qawolf/sandbox": "0.1.19",
"@types/debug": "^4.1.5",
"@types/fs-extra": "^9.0.1",
"@types/glob": "^7.1.3",
Expand Down
2 changes: 1 addition & 1 deletion packages/sandbox/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/sandbox/package.json
@@ -1,6 +1,6 @@
{
"name": "@qawolf/sandbox",
"version": "0.1.18",
"version": "0.1.19",
"files": [
"bin",
"build"
Expand Down
5 changes: 5 additions & 0 deletions packages/sandbox/src/App.js
Expand Up @@ -11,6 +11,7 @@ import CheckboxInputs from './pages/CheckboxInputs';
import ContentEditables from './pages/ContentEditables';
import DatePickers from './pages/DatePickers';
import Images from './pages/Images';
import InlineFrames from './pages/InlineFrames';
import InfiniteScroll from './pages/InfiniteScroll';
import Large from './pages/Large';
import LogIn from './pages/LogIn';
Expand Down Expand Up @@ -41,6 +42,9 @@ function Navigation() {
<li>
<Link to="/images">Images</Link>
</li>
<li>
<Link to="/iframes">Inline Frames</Link>
</li>
<li>
<Link to="/infinite-scroll">Infinite scroll</Link>
</li>
Expand Down Expand Up @@ -82,6 +86,7 @@ function App() {
<Route component={ContentEditables} path="/content-editables" />
<Route component={DatePickers} path="/date-pickers" />
<Route component={Images} path="/images" />
<Route component={InlineFrames} path="/iframes" />
<Route component={InfiniteScroll} path="/infinite-scroll" />
<Route component={Large} path="/large" />
<Route component={LogIn} path="/login" />
Expand Down
36 changes: 36 additions & 0 deletions packages/sandbox/src/pages/InlineFrames/index.js
@@ -0,0 +1,36 @@
import React from "react";

const style = {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
height: '100vh',
position: 'absolute',
alignContent: 'space-around',
width: '100vw'
};

const frameStyle = {
flexGrow: 1,
margin: 20,
width: 'calc(100% - 40px)'
};

const buttonStyle = {
flexGrow: 0,
height: 30,
margin: 20,
width: 'calc(100% - 40px)'
};

function InlineFrames() {
return (
<div style={style}>
<button type="button" style={buttonStyle}>Main Page Button</button>
<iframe src="/buttons" style={frameStyle} title="Buttons" data-qa="first" />
<iframe src="/text-inputs" style={frameStyle} title="Text inputs" data-qa="second" />
</div>
);
}

export default InlineFrames;
72 changes: 54 additions & 18 deletions src/build-code/buildStepLines.ts
@@ -1,19 +1,21 @@
import { ScrollValue, Step } from '../types';
import { isUndefined } from 'util';

export const didPageChange = (step: Step, previous?: Step): boolean => {
if (!previous) return false;

return step.event.page !== previous.event.page;
export type StepLineBuildContext = {
initializedFrames: Map<string, string>;
initializedPages: Set<number>;
};

export const buildPageLine = (step: Step): string => {
return `page = await qawolf.waitForPage(page.context(), ${step.event.page});`;
/**
* @summary Given a step, returns the correct page variable for it,
* such as `page` for the main page or `page2` for the second page.
*/
export const getStepPageVariableName = (step: Step): string => {
const { page } = step.event;
return `page${page === 0 ? '' : page + 1}`;
};

export const buildSelector = (step: Step): string => {
const { selector } = step.event;

export const escapeSelector = (selector: string): string => {
if (!selector.includes(`"`)) return `"${selector}"`;
if (!selector.includes(`'`)) return `'${selector}'`;

Expand All @@ -31,31 +33,65 @@ export const buildValue = ({ action, value }: Step): string => {
return JSON.stringify(value);
};

export const buildExpressionLine = (step: Step): string => {
const { action } = step;
export const buildExpressionLine = (
step: Step,
frameVariable?: string,
): string => {
const { action, event } = step;

const args: string[] = [buildSelector(step)];
const args: string[] = [escapeSelector(event.selector)];

const value = buildValue(step);
if (value) args.push(value);

let methodOpen = `page.${action}(`;
const browsingContext = frameVariable || getStepPageVariableName(step);

let methodOpen = `${browsingContext}.${action}(`;
if (action === 'scroll') {
methodOpen = `qawolf.scroll(page, `;
methodOpen = `qawolf.scroll(${browsingContext}, `;
}

const expression = `await ${methodOpen}${args.join(', ')});`;
return expression;
};

export const buildStepLines = (step: Step, previous?: Step): string[] => {
export const buildStepLines = (
step: Step,
buildContext: StepLineBuildContext = {
initializedFrames: new Map<string, string>(),
initializedPages: new Set(),
},
): string[] => {
const lines: string[] = [];

if (didPageChange(step, previous)) {
lines.push(buildPageLine(step));
const { frameSelector, page } = step.event;
const { initializedFrames, initializedPages } = buildContext;

const pageVariableName = getStepPageVariableName(step);
if (page > 0 && !initializedPages.has(page)) {
lines.push(
`const ${pageVariableName} = await qawolf.waitForPage(page.context(), ${page});`,
);
initializedPages.add(page);
}

let frameVariableName: string;
if (frameSelector) {
frameVariableName = initializedFrames.get(frameSelector);
if (!frameVariableName) {
frameVariableName = `frame${
initializedFrames.size ? initializedFrames.size + 1 : ''
}`;
lines.push(
`const ${frameVariableName} = await (await ${pageVariableName}.$(${escapeSelector(
frameSelector,
)})).contentFrame();`,
);
initializedFrames.set(frameSelector, frameVariableName);
}
}

lines.push(buildExpressionLine(step));
lines.push(buildExpressionLine(step, frameVariableName));

return lines;
};
10 changes: 6 additions & 4 deletions src/build-code/buildVirtualCode.ts
@@ -1,14 +1,16 @@
import { buildStepLines } from './buildStepLines';
import { buildStepLines, StepLineBuildContext } from './buildStepLines';
import { Step } from '../types';
import { VirtualCode } from './VirtualCode';

export const buildVirtualCode = (steps: Step[]): VirtualCode => {
let previous: Step = null;
const lines: string[] = [];
const buildContext: StepLineBuildContext = {
initializedFrames: new Map<string, string>(),
initializedPages: new Set(),
};

steps.forEach(step => {
lines.push(...buildStepLines(step, previous));
previous = step;
lines.push(...buildStepLines(step, buildContext));
});

return new VirtualCode(lines);
Expand Down
50 changes: 46 additions & 4 deletions src/create-code/ContextEventCollector.ts
@@ -1,14 +1,51 @@
import Debug from 'debug';
import { EventEmitter } from 'events';
import { BrowserContext } from 'playwright';
import { BrowserContext, Frame, Page } from 'playwright';
import { loadConfig } from '../config';
import { ElementEvent } from '../types';
import { IndexedPage } from '../utils/context/indexPages';
import { QAWolfWeb } from '../web';
import { DEFAULT_ATTRIBUTE_LIST } from '../web/attribute';
import { isRegistered } from '../utils/context/register';
import { ElementEvent } from '../types';

const debug = Debug('qawolf:ContextEventCollector');

type BindingOptions = {
frame: Frame;
page: Page;
};

export const buildFrameSelector = async (
frame: Frame,
attributes: string[],
): Promise<string> => {
// build the frame selector if this is one frame down from the parent
const parentFrame = frame.parentFrame();

if (parentFrame && !parentFrame.parentFrame()) {
const frameElement = await frame.frameElement();

const frameSelector = await parentFrame.evaluate(
({ attributes, frameElement }) => {
const web: QAWolfWeb = (window as any).qawolf;

return web.buildSelector({
attributes,
isClick: false,
target: frameElement as HTMLElement,
});
},
{ attributes, frameElement },
);

return frameSelector;
}

return undefined;
};

export class ContextEventCollector extends EventEmitter {
readonly _attribute: string;
readonly _attributes: string[];
readonly _context: BrowserContext;

public static async create(
Expand All @@ -21,6 +58,9 @@ export class ContextEventCollector extends EventEmitter {

protected constructor(context: BrowserContext) {
super();
this._attributes = (loadConfig().attribute || DEFAULT_ATTRIBUTE_LIST).split(
',',
);
this._context = context;
}

Expand All @@ -31,10 +71,12 @@ export class ContextEventCollector extends EventEmitter {

await this._context.exposeBinding(
'qawElementEvent',
({ page }, elementEvent: ElementEvent) => {
async ({ frame, page }: BindingOptions, elementEvent: ElementEvent) => {
const pageIndex = (page as IndexedPage).createdIndex;
const event: ElementEvent = { ...elementEvent, page: pageIndex };
debug(`emit %j`, event);

event.frameSelector = await buildFrameSelector(frame, this._attributes);
this.emit('elementevent', event);
},
);
Expand Down
2 changes: 2 additions & 0 deletions src/types.ts
Expand Up @@ -16,12 +16,14 @@ export interface Doc {
}

export interface ElementEvent {
frameSelector?: string;
isTrusted: boolean;
name: ElementEventName;
page: number;
selector: string;
target: Doc;
time: number;
value?: string | ScrollValue | null;
}

export type ElementEventName =
Expand Down
2 changes: 1 addition & 1 deletion src/web/PageEventCollector.ts
Expand Up @@ -54,7 +54,7 @@ export class PageEventCollector {
const target = event.target as HTMLElement;
const isTargetVisible = isVisible(target, window.getComputedStyle(target));

const elementEvent = {
const elementEvent: types.ElementEvent = {
isTrusted: event.isTrusted && isTargetVisible,
name: eventName,
page: -1, // set in ContextEventCollector
Expand Down
4 changes: 3 additions & 1 deletion src/web/selectorEngine.ts
Expand Up @@ -54,7 +54,9 @@ export const getElementText = (element: HTMLElement): string | undefined => {
};

export const isMatch = ({ selectorParts, target }: IsMatch): boolean => {
const result = querySelectorAll({ parts: selectorParts }, document);
// We must pass `target.ownerDocument` rather than `document`
// because sometimes this is called from other frames.
const result = querySelectorAll({ parts: selectorParts }, target.ownerDocument);

return result[0] === target;
};

0 comments on commit aae323b

Please sign in to comment.