Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"resolutions": {
"@definitelytyped/typescript-versions": "^0.0.40",
"@typescript-eslint/eslint-plugin": "^4.12.0",
"chromedriver": "95.0.0",
"chromedriver": "96.0.0",
"typescript": "^4.1.3",
"yargs-parser": "^13.1.2"
}
Expand Down
293 changes: 200 additions & 93 deletions packages/core/src/constructor.ts

Large diffs are not rendered by default.

25 changes: 12 additions & 13 deletions packages/core/src/interaction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { globals } from '@interactors/globals';
import { ActionOptions, globals } from '@interactors/globals';
import type { FilterObject } from './specification';

const interactionSymbol = Symbol.for('interaction');
Expand Down Expand Up @@ -42,7 +42,6 @@ export interface ReadonlyInteraction<T> extends Interaction<T> {
*/
check: () => Promise<T>;
}

function createInteraction<T>(description: string, action: () => Promise<T>): Interaction<T> {
let promise: Promise<T>;
return {
Expand All @@ -51,32 +50,32 @@ function createInteraction<T>(description: string, action: () => Promise<T>): In
[interactionSymbol]: true,
[Symbol.toStringTag]: `[interaction ${description}]`,
then(onFulfill, onReject) {
if(!promise) { promise = this.action(); }
if(!promise) { promise = action(); }
return promise.then(onFulfill, onReject);
},
catch(onReject) {
if(!promise) { promise = this.action(); }
if(!promise) { promise = action(); }
return promise.catch(onReject);
},
finally(handler) {
if(!promise) { promise = this.action(); }
if(!promise) { promise = action(); }
return promise.finally(handler);
}
}
}

export function interaction<T>(description: string, action: () => Promise<T>): Interaction<T> {
return createInteraction(description, globals.wrapAction(description, action, 'interaction'))
export function interaction<T>(description: string, action: () => Promise<T>, options: ActionOptions): Interaction<T> {
return createInteraction(description, globals.wrapAction(Object.assign(new String(description), { description, action, options }), action, options.type))
}

export function check<T>(description: string, check: () => Promise<T>): ReadonlyInteraction<T> {
return { check() { return this.action() }, ...createInteraction(description, globals.wrapAction(description, check, 'check')) };
export function check<T>(interaction: Interaction<T>): ReadonlyInteraction<T> {
return { check() { return interaction.action() }, ...interaction };
}

export function interactionFilter<T, Q>(description: string, action: () => Promise<T>, filter: (element: Element) => Q): Interaction<T> & FilterObject<Q, Element> {
return { apply: filter, ...interaction(description, action) };
export function interactionFilter<T, Q>(interaction: Interaction<T>, filter: (element: Element) => Q): Interaction<T> & FilterObject<Q, Element> {
return { apply: filter, ...interaction };
}

export function checkFilter<T, Q>(description: string, action: () => Promise<T>, filter: (element: Element) => Q): ReadonlyInteraction<T> & FilterObject<Q, Element> {
return { apply: filter , ...check(description, action) };
export function checkFilter<T, Q>(interaction: Interaction<T>, filter: (element: Element) => Q): ReadonlyInteraction<T> & FilterObject<Q, Element> {
return { apply: filter , ...check(interaction) };
}
23 changes: 19 additions & 4 deletions packages/core/src/matcher.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,43 @@
import isEqual from 'lodash.isequal';
import isEqual from "lodash.isequal";

export interface Matcher<T> {
match(actual: T): boolean;
description(): string;
code?(): string;
}

export type MaybeMatcher<T> = Matcher<T> | T;

export function isMatcher<T>(value: MaybeMatcher<T>): value is Matcher<T> {
return value && typeof (value as Matcher<T>).match === 'function' && typeof (value as Matcher<T>).description === 'function';
return (
value &&
typeof (value as Matcher<T>).match === "function" &&
typeof (value as Matcher<T>).description === "function"
);
}

export function matcherDescription<T>(value: MaybeMatcher<T>): string {
if(isMatcher(value)) {
if (isMatcher(value)) {
return value.description();
} else {
return JSON.stringify(value);
}
}

export function applyMatcher<T>(value: MaybeMatcher<T>, actual: T): boolean {
if(isMatcher(value)) {
if (isMatcher(value)) {
return value.match(actual);
} else {
return isEqual(value, actual);
}
}

export function matcherCode<T>(value: MaybeMatcher<T>): string {
if (isMatcher(value) && value.code) {
return value.code();
} else if (value instanceof RegExp) {
return value.toString();
} else {
return JSON.stringify(value);
}
}
5 changes: 4 additions & 1 deletion packages/core/src/matchers/and.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Matcher, MaybeMatcher, matcherDescription, applyMatcher } from '../matcher';
import { Matcher, MaybeMatcher, matcherDescription, applyMatcher, matcherCode } from '../matcher';

export function and<T>(...args: MaybeMatcher<T>[]): Matcher<T> {
return {
Expand All @@ -8,5 +8,8 @@ export function and<T>(...args: MaybeMatcher<T>[]): Matcher<T> {
description(): string {
return args.map(matcherDescription).join(' and ');
},
code(): string {
return `and(${args.map(arg => matcherCode(arg)).join(', ')})`
}
}
}
5 changes: 4 additions & 1 deletion packages/core/src/matchers/every.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Matcher, MaybeMatcher, applyMatcher, matcherDescription } from '../matcher';
import { Matcher, MaybeMatcher, applyMatcher, matcherDescription, matcherCode } from '../matcher';

export function every<T>(expected: MaybeMatcher<T>): Matcher<Iterable<T>> {
return {
Expand All @@ -8,5 +8,8 @@ export function every<T>(expected: MaybeMatcher<T>): Matcher<Iterable<T>> {
description(): string {
return `every item ${matcherDescription(expected)}`;
},
code(): string {
return `every(${matcherCode(expected)})`
}
}
}
5 changes: 4 additions & 1 deletion packages/core/src/matchers/including.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Matcher } from '../matcher';
import { Matcher, matcherCode } from '../matcher';

export function including(subString: string): Matcher<string> {
return {
Expand All @@ -8,5 +8,8 @@ export function including(subString: string): Matcher<string> {
description(): string {
return `including ${JSON.stringify(subString)}`;
},
code(): string {
return `including(${matcherCode(subString)})`
}
}
}
5 changes: 4 additions & 1 deletion packages/core/src/matchers/matching.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Matcher } from '../matcher';
import { Matcher, matcherCode } from '../matcher';

export function matching(regexp: RegExp): Matcher<string> {
return {
Expand All @@ -8,5 +8,8 @@ export function matching(regexp: RegExp): Matcher<string> {
description(): string {
return `matching ${regexp}`;
},
code(): string {
return `matching(${matcherCode(regexp)})`
}
}
}
7 changes: 5 additions & 2 deletions packages/core/src/matchers/not.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Matcher, MaybeMatcher, matcherDescription, applyMatcher } from '../matcher';
import { Matcher, MaybeMatcher, matcherDescription, applyMatcher, matcherCode } from "../matcher";

export function not<T>(matcher: MaybeMatcher<T>): Matcher<T> {
return {
Expand All @@ -8,5 +8,8 @@ export function not<T>(matcher: MaybeMatcher<T>): Matcher<T> {
description(): string {
return `not ${matcherDescription(matcher)}`;
},
}
code(): string {
return `not(${matcherCode(matcher)})`
},
};
}
5 changes: 4 additions & 1 deletion packages/core/src/matchers/or.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Matcher, MaybeMatcher, matcherDescription, applyMatcher } from '../matcher';
import { Matcher, MaybeMatcher, matcherDescription, applyMatcher, matcherCode } from '../matcher';

export function or<T>(...args: MaybeMatcher<T>[]): Matcher<T> {
return {
Expand All @@ -8,5 +8,8 @@ export function or<T>(...args: MaybeMatcher<T>[]): Matcher<T> {
description(): string {
return args.map(matcherDescription).join(' or ');
},
code(): string {
return `or(${args.map(arg => matcherCode(arg)).join(', ')})`
}
}
}
5 changes: 4 additions & 1 deletion packages/core/src/matchers/some.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Matcher, MaybeMatcher, applyMatcher, matcherDescription } from '../matcher';
import { Matcher, MaybeMatcher, applyMatcher, matcherDescription, matcherCode } from '../matcher';

export function some<T>(expected: MaybeMatcher<T>): Matcher<Iterable<T>> {
return {
Expand All @@ -8,5 +8,8 @@ export function some<T>(expected: MaybeMatcher<T>): Matcher<Iterable<T>> {
description(): string {
return `some item ${matcherDescription(expected)}`;
},
code(): string {
return `some(${matcherCode(expected)})`
}
}
}
12 changes: 12 additions & 0 deletions packages/core/src/specification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,3 +246,15 @@ export type InteractorOptions<E extends Element, F extends Filters<E>, A extends
filter: FilterSet<E, F>;
ancestors: InteractorOptions<any, any, any>[];
};

export type ActionOptions = {
type: "interaction";
actionName: string;
options: InteractorOptions<any, any, any>;
args?: unknown[];
} | {
type: "check";
actionName: string;
options: InteractorOptions<any, any, any>;
filters?: FilterParams<any, any>;
}
84 changes: 84 additions & 0 deletions packages/core/test/create-interactor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import expect from 'expect';
import { dom } from './helpers';

import { createInteractor } from '../src';
import { ActionEvent, addActionWrapper } from '@interactors/globals';

const Link = createInteractor<HTMLLinkElement>('link')
.selector('a')
Expand Down Expand Up @@ -59,6 +60,14 @@ const MainNav = createInteractor('main nav')
.selector('nav')

describe('createInteractor', () => {
let actionCode = ''
before(() => {
addActionWrapper((actionEvent: ActionEvent<unknown>) => {
actionCode = actionEvent.options.code
return actionEvent.action
})
})

describe('.exists', () => {
it('can determine whether an element exists based on the interactor', async () => {
dom(`
Expand Down Expand Up @@ -131,6 +140,14 @@ describe('createInteractor', () => {
it('can return description', () => {
expect(Link('Foo Bar').exists().description).toEqual('link "Foo Bar" exists')
});

it("can be serialized to code representation for action wrapper", () => {
dom(`<p><a href="/foobar">Foo Bar</a></p>`);

Link('Foo Bar').exists()

expect(actionCode).toBe('link("Foo Bar").exists()');
})
});

describe('.absent', () => {
Expand Down Expand Up @@ -183,6 +200,14 @@ describe('createInteractor', () => {
it('can return description', () => {
expect(Link('Foo Bar').absent().description).toEqual('link "Foo Bar" does not exist')
});

it("can be serialized to code representation for action wrapper", () => {
dom(`<p><a href="/foobar">Foo Bar</a></p>`);

Link('Blah').absent()

expect(actionCode).toBe('link("Blah").absent()');
})
});

describe('.find', () => {
Expand Down Expand Up @@ -285,6 +310,18 @@ describe('createInteractor', () => {

await expect(Div("foo").find(Div("bar")).exists()).rejects.toHaveProperty('message', 'did not find div "bar" within div "foo"');
});

it("can be serialized to code representation for action wrapper", () => {
dom(`
<div id="foo">
<a href="/foo">Foo</a>
</div>
`);

Div("foo").find(Link("Foo")).exists()

expect(actionCode).toBe('div("foo").find(link("Foo")).exists()');
})
});

describe('.is', () => {
Expand All @@ -301,6 +338,14 @@ describe('createInteractor', () => {
'└─ Received: "jonas@example.com"',
].join('\n'))
});

it("can be serialized to code representation for action wrapper", () => {
dom(`<input id="Email" value='jonas@example.com'/>`);

TextField('Email').is({ value: 'jonas@example.com' })

expect(actionCode).toBe('text field("Email", { "enabled": true }).is({ "value": "jonas@example.com" })');
})
});

describe('.has', () => {
Expand All @@ -317,6 +362,14 @@ describe('createInteractor', () => {
'└─ Received: "jonas@example.com"',
].join('\n'))
});

it("can be serialized to code representation for action wrapper", () => {
dom(`<input id="Email" value='jonas@example.com'/>`);

TextField('Email').has({ value: 'jonas@example.com' })

expect(actionCode).toBe('text field("Email", { "enabled": true }).is({ "value": "jonas@example.com" })');
})
});

describe('actions', () => {
Expand Down Expand Up @@ -435,6 +488,16 @@ describe('createInteractor', () => {
'- <a href="/bar&quot;">',
].join('\n'))
});

it("can be serialized to code representation for action wrapper", () => {
dom(`
<a id="foo" href="/foobar">Foo Bar</a>
`);

Link('Foo Bar').setHref('/monkey');

expect(actionCode).toBe('link("Foo Bar").setHref("/monkey")');
})
});

describe('filters', () => {
Expand Down Expand Up @@ -537,6 +600,16 @@ describe('createInteractor', () => {
].join('\n'))
await expect(TextField('Password', { enabled: false, value: 'test1234' }).exists()).resolves.toBeUndefined();
});

it("can be serialized to code representation for action wrapper", () => {
dom(`
<input id="Email" value='jonas@example.com'/>
`);

TextField('Email', { value: 'jonas@example.com' }).exists();

expect(actionCode).toBe('text field("Email", { "value": "jonas@example.com", "enabled": true }).exists()');
})
});

describe('getters', () => {
Expand All @@ -549,5 +622,16 @@ describe('createInteractor', () => {
await expect(TextField('Password').value()).resolves.toEqual('test1234')
await expect(TextField({ value: 'jonas@example.com' }).id()).resolves.toEqual('Email')
})

it("can be serialized to code representation for action wrapper", () => {
dom(`
<input id="Email" value='jonas@example.com'/>
<input id="Password" value='test1234'/>
`);

TextField('Password').value()

expect(actionCode).toBe('text field("Password", { "enabled": true }).value()');
})
})
});
Loading