Skip to content

Commit

Permalink
feat: record and generate nav steps
Browse files Browse the repository at this point in the history
  • Loading branch information
aldeed authored and jperl committed Sep 21, 2020
1 parent ece8074 commit e4195f5
Show file tree
Hide file tree
Showing 16 changed files with 217 additions and 58 deletions.
2 changes: 1 addition & 1 deletion packages/sandbox/src/pages/TextInputs/HtmlTextInputs.js
Expand Up @@ -46,7 +46,7 @@ function HtmlTextInputs() {
<br />
<h4>Edge Cases</h4>
<input
contenteditable="true"
contentEditable="true"
data-qa="html-text-input-content-editable"
placeholder="Content editable text input"
type="text"
Expand Down
34 changes: 24 additions & 10 deletions src/build-code/buildStepLines.ts
@@ -1,5 +1,4 @@
import { ScrollValue, Step } from '../types';
import { isUndefined } from 'util';
import { ElementEvent, ScrollValue, Step } from '../types';

export type StepLineBuildContext = {
initializedFrames: Map<string, string>;
Expand Down Expand Up @@ -29,7 +28,7 @@ export const buildValue = ({ action, value }: Step): string => {
return `{ x: ${scrollValue.x}, y: ${scrollValue.y} }`;
}

if (isUndefined(value)) return '';
if (value === undefined) return '';

return JSON.stringify(value);
};
Expand All @@ -40,11 +39,18 @@ export const buildExpressionLine = (
): string => {
const { action, event } = step;

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

const selector = (event as ElementEvent).selector;
if (selector) args.push(escapeSelector(selector));

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

if (['goto', 'goBack', 'goForward', 'reload'].includes(action)) {
args.push('{ waitUntil: "domcontentloaded" }');
}

const browsingContext = frameVariable || getStepPageVariableName(step);

let methodOpen = `${browsingContext}.${action}(`;
Expand All @@ -60,7 +66,7 @@ export const buildStepLines = (
step: Step,
buildContext: StepLineBuildContext = {
initializedFrames: new Map<string, string>(),
initializedPages: new Set([0]),
initializedPages: new Set([]),
visiblePage: 0,
},
): string[] => {
Expand All @@ -76,12 +82,20 @@ export const buildStepLines = (
// Otherwise, if we were doing steps on a different page and have now switched back
// to this one, add a `bringToFront` call. Otherwise no extra page-waiting line is needed.
if (!initializedPages.has(page)) {
lines.push(
`const ${pageVariableName} = await qawolf.waitForPage(page.context(), ${page});`,
);
if (step.action === 'goto') {
lines.push(
`const ${pageVariableName} = await context.newPage();`,
);
} else {
lines.push(
`const ${pageVariableName} = await qawolf.getPageAtIndex(context, ${page}, { waitUntil: "domcontentloaded" });`,
`await ${pageVariableName}.waitForLoadState("domcontentloaded");`
);
}
initializedPages.add(page);
buildContext.visiblePage = page;
} else if (buildContext.visiblePage !== page) {
}

if (buildContext.visiblePage !== page) {
lines.push(
`await ${pageVariableName}.bringToFront();`,
);
Expand Down
6 changes: 2 additions & 4 deletions src/build-code/buildTemplate.ts
Expand Up @@ -96,17 +96,15 @@ beforeAll(async () => {
browser = await qawolf.launch();
${buildNewContext(device)}
await qawolf.register(context);
page = await context.newPage();
});
afterAll(async () => {
await qawolf.stopVideos();
await browser.close();
});
test("${name}", async () => {
await page.goto("${url}");${buildSetState(statePath)}
await qawolf.create();
test("${name}", async () => {${buildSetState(statePath)}
await qawolf.create("${url}");
});`;

return code;
Expand Down
3 changes: 1 addition & 2 deletions src/build-code/buildVirtualCode.ts
Expand Up @@ -7,8 +7,7 @@ export const buildVirtualCode = (steps: Step[]): VirtualCode => {

const buildContext: StepLineBuildContext = {
initializedFrames: new Map<string, string>(),
// page 0 is initialized and brought to front in "before"
initializedPages: new Set([0]),
initializedPages: new Set([]),
visiblePage: 0,
};

Expand Down
10 changes: 10 additions & 0 deletions src/build-workflow/buildNavigationSteps.ts
@@ -0,0 +1,10 @@
import { Action, WindowEvent, Step } from '../types';

export const buildNavigationSteps = (events: WindowEvent[]): Step[] => {
return events.map((event, index) => ({
action: (event.name as Action),
event,
index,
value: event.value,
}));
};
16 changes: 9 additions & 7 deletions src/build-workflow/buildSteps.ts
@@ -1,18 +1,20 @@
import { concat, sortBy } from 'lodash';
import { buildClickSteps } from './buildClickSteps';
import { buildFillSteps } from './buildFillSteps';
import { buildNavigationSteps } from './buildNavigationSteps';
import { buildPressSteps } from './buildPressSteps';
import { buildScrollSteps } from './buildScrollSteps';
import { buildSelectOptionSteps } from './buildSelectOptionSteps';
import { ElementEvent, Step } from '../types';
import { ElementEvent, Step, WindowEvent } from '../types';

export const buildSteps = (events: ElementEvent[]): Step[] => {
export const buildSteps = (elementEvents: ElementEvent[], windowEvents: WindowEvent[] = []): Step[] => {
const unorderedSteps = concat(
buildClickSteps(events),
buildFillSteps(events),
buildPressSteps(events),
buildScrollSteps(events),
buildSelectOptionSteps(events),
buildClickSteps(elementEvents),
buildFillSteps(elementEvents),
buildPressSteps(elementEvents),
buildScrollSteps(elementEvents),
buildSelectOptionSteps(elementEvents),
buildNavigationSteps(windowEvents),
);

let steps = sortBy(
Expand Down
4 changes: 2 additions & 2 deletions src/create-code/CodeUpdater.ts
Expand Up @@ -3,7 +3,7 @@ import { EventEmitter } from 'events';
import { buildVirtualCode } from '../build-code/buildVirtualCode';
import { CodeReconciler } from './CodeReconciler';
import { getLineIncludes, removeLinesIncluding } from './format';
import { PATCH_HANDLE } from './patchCode';
import { CREATE_HANDLE, PATCH_HANDLE } from './patchCode';
import { Step } from '../types';

const debug = Debug('qawolf:CodeUpdater');
Expand Down Expand Up @@ -37,7 +37,7 @@ export abstract class CodeUpdater extends EventEmitter {
protected async _prepare(): Promise<void> {
const code = await this._loadCode();

const createLine = getLineIncludes(code, `qawolf.create()`);
const createLine = getLineIncludes(code, CREATE_HANDLE);
if (!createLine) return;

const updatedCode = code.replace(createLine.trim(), PATCH_HANDLE);
Expand Down
95 changes: 92 additions & 3 deletions src/create-code/ContextEventCollector.ts
@@ -1,12 +1,12 @@
import Debug from 'debug';
import { EventEmitter } from 'events';
import { BrowserContext, Frame, Page } from 'playwright';
import { BrowserContext, Frame, Page, ChromiumBrowserContext, CDPSession } from 'playwright';
import { loadConfig } from '../config';
import { ElementEvent } from '../types';
import { ElementEvent, WindowEvent, WindowEventName } from '../types';
import { IndexedPage } from '../utils/context/indexPages';
import { QAWolfWeb } from '../web';
import { DEFAULT_ATTRIBUTE_LIST } from '../web/attribute';
import { forEachFrame } from '../utils/context/forEach';
import { forEachFrame, forEachPage } from '../utils/context/forEach';
import { isRegistered } from '../utils/context/register';

const debug = Debug('qawolf:ContextEventCollector');
Expand All @@ -21,6 +21,11 @@ type FrameSelector = {
selector: string;
};

type LastPageNavigation = {
lastHistoryEntriesLength: number;
lastHistoryIndex: number;
};

export const buildFrameSelector = async (
frame: Frame,
attributes: string[],
Expand Down Expand Up @@ -51,9 +56,11 @@ export const buildFrameSelector = async (
};

export class ContextEventCollector extends EventEmitter {
readonly _activeSessions = new Set<CDPSession>();
readonly _attributes: string[];
readonly _context: BrowserContext;
readonly _frameSelectors = new Map<Frame, FrameSelector>();
readonly _pageNavigationHistory = new Map<Page, LastPageNavigation>();

public static async create(
context: BrowserContext,
Expand Down Expand Up @@ -108,5 +115,87 @@ export class ContextEventCollector extends EventEmitter {
this.emit('elementevent', event);
},
);

await forEachPage(this._context, async (page) => {
const pageIndex = (page as IndexedPage).createdIndex;

// Currently only ChromiumBrowserContext can do CDP, so we cannot support adding
// new tabs manually or back/forward/reload on other browsers
if ((this._context as any)._browser._options.name === 'chromium') {
const session = await (this._context as ChromiumBrowserContext).newCDPSession(page);
const { currentIndex, entries } = await session.send("Page.getNavigationHistory");
console.log("initial", currentIndex, entries);

const currentHistoryEntry = entries[currentIndex];
if (currentHistoryEntry.transitionType === 'typed' && currentHistoryEntry.url !== 'chrome://newtab/') {
this.emit('windowevent', {
name: 'goto',
page: pageIndex,
time: Date.now(),
value: currentHistoryEntry.url,
});
}

this._pageNavigationHistory.set(page, {
lastHistoryIndex: currentIndex,
lastHistoryEntriesLength: entries.length,
});

page.on('framenavigated', async (frame) => {
if (frame.parentFrame()) return;

const { currentIndex, entries } = await session.send("Page.getNavigationHistory");
const currentHistoryEntry = entries[currentIndex];
console.log("updated", currentIndex, entries);

const { lastHistoryEntriesLength, lastHistoryIndex } = this._pageNavigationHistory.get(page);

let name: WindowEventName;
let url: string;

if (entries.length > lastHistoryEntriesLength && currentHistoryEntry.transitionType === 'typed') {
// NEW ADDRESS ENTERED
name = 'goto';
url = currentHistoryEntry.url;
} else if (lastHistoryEntriesLength === entries.length) {
if (currentIndex < lastHistoryIndex) {
// BACK
name = 'goBack';
} else if (currentIndex > lastHistoryIndex) {
// FORWARD OR NEW ADDRESS ENTERED
name = 'goForward';
} else if (currentIndex === lastHistoryIndex && currentHistoryEntry.transitionType === 'reload') {
// RELOAD
name = 'reload';
}
}

this._pageNavigationHistory.set(page, {
lastHistoryIndex: currentIndex,
lastHistoryEntriesLength: entries.length,
});

if (!name) return;

const event: WindowEvent = {
name,
page: pageIndex,
time: Date.now(),
value: url,
};

this.emit('windowevent', event);
});

this._activeSessions.add(session);
} else if (pageIndex === 0) {
this.emit('windowevent', {
name: 'goto',
page: 0,
time: Date.now(),
value: page.url(),
});
}
});
}
}
14 changes: 10 additions & 4 deletions src/create-code/CreateManager.ts
Expand Up @@ -4,7 +4,7 @@ import { buildSteps } from '../build-workflow/buildSteps';
import { CodeFileUpdater } from './CodeFileUpdater';
import { ContextEventCollector } from './ContextEventCollector';
import { createPrompt } from './createPrompt';
import { ElementEvent } from '../types';
import { ElementEvent, PageEvent, WindowEvent } from '../types';

type CreateCliOptions = {
codePath: string;
Expand Down Expand Up @@ -39,18 +39,24 @@ export class CreateManager {
private _codeUpdater: CodeFileUpdater;
private _collector: ContextEventCollector;
private _events: ElementEvent[] = [];
private _windowEvents: WindowEvent[] = [];

protected constructor(options: ConstructorOptions) {
this._codeUpdater = options.codeUpdater;
this._collector = options.collector;

this._collector.on('elementevent', (event) => this.update(event));
this._collector.on('windowevent', (event) => this.update(event, true));
}

protected async update(event: ElementEvent): Promise<void> {
this._events.push(event);
protected async update(event: PageEvent, isWindowEvent = false): Promise<void> {
if (isWindowEvent) {
this._windowEvents.push(event as WindowEvent);
} else {
this._events.push(event as ElementEvent);
}

const steps = buildSteps(this._events);
const steps = buildSteps(this._events, this._windowEvents);
await this._codeUpdater.update({ steps });
}

Expand Down
7 changes: 6 additions & 1 deletion src/create-code/create.ts
Expand Up @@ -39,7 +39,7 @@ export const getCreatePath = async (
return item.filename;
};

export const create = async (): Promise<void> => {
export const create = async (url?: string): Promise<void> => {
const registryData = Registry.instance().data();
const context = registryData.context;
if (!context) {
Expand All @@ -57,6 +57,11 @@ export const create = async (): Promise<void> => {
context,
});

if (url) {
const firstPage = await context.newPage();
await firstPage.goto(url);
}

console.log(bold().blue('🐺 QA Wolf is ready to create code!'));

await manager.finalize();
Expand Down
2 changes: 1 addition & 1 deletion src/create-code/patchCode.ts
Expand Up @@ -5,7 +5,7 @@ type PatchOptions = {
patch: string;
};

export const CREATE_HANDLE = 'qawolf.create()';
export const CREATE_HANDLE = 'qawolf.create(';

export const PATCH_HANDLE = '// 🐺 create code here';

Expand Down

0 comments on commit e4195f5

Please sign in to comment.