Skip to content

Commit

Permalink
fix(core): screenplay Adapters will now correctly proxy calls to func…
Browse files Browse the repository at this point in the history
…tion-specific object keys

Adapter created by Question.about used to hijack calls to properties called "arguments", "caller",
"length", "name" because they exist as properties on the adapter function itself
  • Loading branch information
jan-molak committed Jan 3, 2022
1 parent 126a1ca commit ad6f1e6
Show file tree
Hide file tree
Showing 2 changed files with 57 additions and 2 deletions.
52 changes: 52 additions & 0 deletions packages/core/spec/screenplay/Question.Adapter.spec.ts
Expand Up @@ -577,10 +577,62 @@ describe('Question', () => {
});
});
});

describe(`correctly proxies calls to fields, even if they exist on the proxy itself`, () => {

const Response = () =>
Question.about<{ users: User[] }>(`users`, (actor: Actor) => ({
users: [
// "caller" and "argument" are here to show how a proxy resolves conflicts
// between a built-in fields like function.caller and function.arguments
// and properties of the wrapped object of the same name
{ name: 'Alice', caller: 'Bob', arguments: false },
{ name: 'Bob', caller: 'Alice', arguments: true },
]
}));

it('proxies "arguments"', async () => {
const args: Question<Promise<boolean>> = Response().users[0].arguments;

const result: boolean = await args.answeredBy(Quentin);

expect(result).to.equal(false);
});

it('proxies "caller"', async () => {
const caller: Question<Promise<string>> = Response().users[0].caller;

const result: string = await caller.answeredBy(Quentin);

expect(result).to.equal('Bob');
});

it('proxies "length"', async () => {
const length: Question<Promise<number>> = Response().users.length;

const result: number = await length.answeredBy(Quentin);

expect(result).to.equal(2);
});

it('proxies "name"', async () => {
const name: Question<Promise<string>> = Response().users[0].name;

const result: string = await name.answeredBy(Quentin);

expect(result).to.equal('Alice');
});
});
});
});
});

interface User {
name: string;
caller: string;
arguments: boolean;
}

class Counter {
constructor(public current: number = 0) {
}
Expand Down
7 changes: 5 additions & 2 deletions packages/core/src/screenplay/model/createAdapter.ts
Expand Up @@ -104,10 +104,13 @@ export function createAdapter<A, Q extends Question<A> = Question<A>>(question:
}

function shouldReflect<A, Q extends Question<A> = Question<A>>(target: Q & { name?: string }, key: string | symbol | number): boolean {

const proxyKeys: Array<string | symbol | number> = ['arguments', 'caller', 'length', 'name'];

// return the actual value
return key in target
// unless target is a proxy function, because proxy function has a built-in .length attribute that could shadow Array.length of the wrapped object
&& !((target as any).name === '__serenityProxy' && key === 'length')
// unless target is a proxy function, because proxy function has built-in .length and .name attribute that could shadow Array.length of the wrapped object
&& !((target as any).name === '__serenityProxy' && (proxyKeys.includes(key)))
}

function shouldProxy(key: string | symbol | number) {
Expand Down

0 comments on commit ad6f1e6

Please sign in to comment.