-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add world and context proxies for use in arrow functions (#2402)
- Loading branch information
1 parent
2990fe4
commit 1901ec8
Showing
15 changed files
with
309 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
Feature: Scope proxies | ||
|
||
Background: | ||
Given a file named "features/a.feature" with: | ||
""" | ||
Feature: some feature | ||
Scenario: some scenario | ||
Given a step | ||
""" | ||
And a file named "features/support/world.js" with: | ||
""" | ||
const {setWorldConstructor,World} = require('@cucumber/cucumber') | ||
setWorldConstructor(class WorldConstructor extends World { | ||
isWorld() { return true } | ||
}) | ||
""" | ||
And a file named "cucumber.json" with: | ||
""" | ||
{ | ||
"default": { | ||
"worldParameters": { | ||
"a": 1 | ||
} | ||
} | ||
} | ||
""" | ||
|
||
Scenario: world and context can be used from appropriate scopes | ||
Given a file named "features/step_definitions/cucumber_steps.js" with: | ||
""" | ||
const {BeforeAll,Given,BeforeStep,Before,world,context} = require('@cucumber/cucumber') | ||
const assert = require('node:assert/strict') | ||
BeforeAll(() => assert.equal(context.parameters.a, 1)) | ||
Given('a step', () => assert(world.isWorld())) | ||
BeforeStep(() => assert(world.isWorld())) | ||
Before(() => assert(world.isWorld())) | ||
""" | ||
When I run cucumber-js | ||
Then it passes | ||
|
||
Scenario: world proxy cannot be used outside correct scope | ||
Given a file named "features/step_definitions/cucumber_steps.js" with: | ||
""" | ||
const {BeforeAll,world} = require('@cucumber/cucumber') | ||
const assert = require('node:assert/strict') | ||
BeforeAll(() => assert(world.isWorld())) | ||
""" | ||
When I run cucumber-js | ||
Then it fails | ||
|
||
Scenario: context proxy cannot be used outside correct scope | ||
Given a file named "features/step_definitions/cucumber_steps.js" with: | ||
""" | ||
const {Given,context} = require('@cucumber/cucumber') | ||
const assert = require('node:assert/strict') | ||
Given(() => console.log(context.parameters)) | ||
""" | ||
When I run cucumber-js | ||
Then it fails |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './test_case_scope' | ||
export * from './test_run_scope' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
export function makeProxy<T>(getThing: () => any): T { | ||
return new Proxy( | ||
{}, | ||
{ | ||
defineProperty(_, property, attributes) { | ||
return Reflect.defineProperty(getThing(), property, attributes) | ||
}, | ||
deleteProperty(_, property) { | ||
return Reflect.get(getThing(), property) | ||
}, | ||
get(_, property) { | ||
return Reflect.get(getThing(), property, getThing()) | ||
}, | ||
getOwnPropertyDescriptor(_, property) { | ||
return Reflect.getOwnPropertyDescriptor(getThing(), property) | ||
}, | ||
getPrototypeOf(_) { | ||
return Reflect.getPrototypeOf(getThing()) | ||
}, | ||
has(_, key) { | ||
return Reflect.has(getThing(), key) | ||
}, | ||
isExtensible(_) { | ||
return Reflect.isExtensible(getThing()) | ||
}, | ||
ownKeys(_) { | ||
return Reflect.ownKeys(getThing()) | ||
}, | ||
preventExtensions(_) { | ||
return Reflect.preventExtensions(getThing()) | ||
}, | ||
set(_, property, value) { | ||
return Reflect.set(getThing(), property, value, getThing()) | ||
}, | ||
setPrototypeOf(_, proto) { | ||
return Reflect.setPrototypeOf(getThing(), proto) | ||
}, | ||
} | ||
) as T | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { AsyncLocalStorage } from 'node:async_hooks' | ||
import { IWorld } from '../../support_code_library_builder/world' | ||
import { makeProxy } from './make_proxy' | ||
|
||
interface TestCaseScopeStore<ParametersType = any> { | ||
world: IWorld<ParametersType> | ||
} | ||
|
||
const testCaseScope = new AsyncLocalStorage<TestCaseScopeStore>() | ||
|
||
export async function runInTestCaseScope<ResponseType>( | ||
store: TestCaseScopeStore, | ||
callback: () => ResponseType | ||
) { | ||
return testCaseScope.run(store, callback) | ||
} | ||
|
||
function getWorld<ParametersType = any>(): IWorld<ParametersType> { | ||
const store = testCaseScope.getStore() | ||
if (!store) { | ||
throw new Error( | ||
'Attempted to access `world` from incorrect scope; only applicable to steps and case-level hooks' | ||
) | ||
} | ||
return store.world as IWorld<ParametersType> | ||
} | ||
|
||
/** | ||
* A proxy to the World instance for the currently-executing test case | ||
* | ||
* @beta | ||
* @remarks | ||
* Useful for getting a handle on the World when using arrow functions and thus | ||
* being unable to rely on the value of `this`. Only callable from the body of a | ||
* step or a `Before`, `After`, `BeforeStep` or `AfterStep` hook (will throw | ||
* otherwise). | ||
*/ | ||
export const worldProxy = makeProxy<IWorld>(getWorld) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import sinon from 'sinon' | ||
import { expect } from 'chai' | ||
import World from '../../support_code_library_builder/world' | ||
import { ICreateAttachment } from '../attachment_manager' | ||
import { IFormatterLogFn } from '../../formatter' | ||
import { runInTestCaseScope, worldProxy } from './test_case_scope' | ||
|
||
describe('testCaseScope', () => { | ||
class CustomWorld extends World { | ||
firstNumber: number = 0 | ||
secondNumber: number = 0 | ||
|
||
get numbers() { | ||
return [this.firstNumber, this.secondNumber] | ||
} | ||
|
||
sum() { | ||
return this.firstNumber + this.secondNumber | ||
} | ||
} | ||
|
||
it('provides a proxy to the world that works when running a test case', async () => { | ||
const customWorld = new CustomWorld({ | ||
attach: sinon.stub() as unknown as ICreateAttachment, | ||
log: sinon.stub() as IFormatterLogFn, | ||
parameters: {}, | ||
}) | ||
const customProxy = worldProxy as CustomWorld | ||
|
||
await runInTestCaseScope({ world: customWorld }, () => { | ||
// simple property access | ||
customProxy.firstNumber = 1 | ||
customProxy.secondNumber = 2 | ||
expect(customProxy.firstNumber).to.eq(1) | ||
expect(customProxy.secondNumber).to.eq(2) | ||
// getters using internal state | ||
expect(customProxy.numbers).to.deep.eq([1, 2]) | ||
// instance methods using internal state | ||
expect(customProxy.sum()).to.eq(3) | ||
// enumeration | ||
expect(Object.keys(customProxy)).to.deep.eq([ | ||
'attach', | ||
'log', | ||
'parameters', | ||
'firstNumber', | ||
'secondNumber', | ||
]) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { AsyncLocalStorage } from 'node:async_hooks' | ||
import { IContext } from '../../support_code_library_builder/context' | ||
import { makeProxy } from './make_proxy' | ||
|
||
interface TestRunScopeStore<ParametersType = any> { | ||
context: IContext<ParametersType> | ||
} | ||
|
||
const testRunScope = new AsyncLocalStorage<TestRunScopeStore>() | ||
|
||
export async function runInTestRunScope<ResponseType>( | ||
store: TestRunScopeStore, | ||
callback: () => ResponseType | ||
) { | ||
return testRunScope.run(store, callback) | ||
} | ||
|
||
function getContext<ParametersType = any>(): IContext<ParametersType> { | ||
const store = testRunScope.getStore() | ||
if (!store) { | ||
throw new Error( | ||
'Attempted to access `context` from incorrect scope; only applicable to run-level hooks' | ||
) | ||
} | ||
return store.context as IContext<ParametersType> | ||
} | ||
|
||
/** | ||
* A proxy to the context for the currently-executing test run. | ||
* | ||
* @beta | ||
* @remarks | ||
* Useful for getting a handle on the context when using arrow functions and thus | ||
* being unable to rely on the value of `this`. Only callable from the body of a | ||
* `BeforeAll` or `AfterAll` hook (will throw otherwise). | ||
*/ | ||
export const contextProxy = makeProxy<IContext>(getContext) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { expect } from 'chai' | ||
import { contextProxy, runInTestRunScope } from './test_run_scope' | ||
|
||
describe('testRunScope', () => { | ||
it('provides a proxy to the context that works when running a test run hook', async () => { | ||
const context = { | ||
parameters: { | ||
foo: 1, | ||
bar: 2, | ||
}, | ||
} | ||
|
||
await runInTestRunScope({ context }, () => { | ||
// simple property access | ||
expect(contextProxy.parameters.foo).to.eq(1) | ||
contextProxy.parameters.foo = 'baz' | ||
expect(contextProxy.parameters.foo).to.eq('baz') | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.