Skip to content

Commit

Permalink
Add context provider test helpers (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinkucharczyk authored Mar 23, 2024
1 parent b7e57d2 commit 611b1df
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changeset/serious-lemons-pull.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ember-provide-consume-context": patch
---

Add context provider test helpers
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,83 @@ export default class MyChildComponent extends Component {
}
```

### Testing
The addon includes two test helpers for setting up context providers in integration tests:
- `provide`
- `setupRenderWrapper`

#### `provide`
The `provide` helper can be used to set up a global context provider for a test, by calling
it with a context key and its value, for example:

```ts
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { provide } from 'ember-provide-consume-context/test-support';

module('component tests', function (hooks) {
setupRenderingTest(hooks);

hooks.beforeEach(function (this) {
provide('my-test-context', {
count: 1,
});
});

test('can read context', async function (assert) {
await render(hbs`
<ContextConsumer @key="my-test-context" as |data|>
<div id="content">{{data.count}}</div>
</ContextConsumer>
`);

assert.dom('#content').hasText('1');
});
});
```

#### `setupRenderWrapper`
`setupRenderWrapper` can be used to define a template to wrap contents rendered via `render` in a test.

This is useful, for example, when it's necessary to wrap content in a context provider exposed by
and addon, or an internal context provider from the application. For example:

```ts
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupRenderWrapper } from 'ember-provide-consume-context/test-support';

module('component tests', function (hooks) {
setupRenderingTest(hooks);

hooks.beforeEach(function (this) {
setupRenderWrapper(hbs`
<ThemeProvider @mode="dark">
{{outlet}}
</ThemeProvider>
`);
});

test('can read context', async function (assert) {
await render(hbs`
<DesignSystemButton>Button</DesignSystemButton>
`);

// renders in dark mode
});
});
```

> [!IMPORTANT]
> The render wrapper must use `{{outlet}}` rather than `{{yield}}` to render the wrapped content.
>
> Internally, `setupRenderWrapper` overrrides the outlet template defined by `@ember/test-helpers`: https://github.com/emberjs/ember-test-helpers/blob/9cec68dc6aa9c0a7a449eb89797eb81299fa727f/addon/addon-test-support/%40ember/test-helpers/setup-rendering-context.ts#L68-L69

## Inspiration
The idea was to create an API similar to the Context API in React
- [`React Context API`](https://react.dev/reference/react/createContext): The
Expand Down
8 changes: 7 additions & 1 deletion ember-provide-consume-context/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
"types": "./declarations/index.d.ts",
"default": "./dist/index.js"
},
"./test-support": {
"types": "./declarations/test-support/index.d.ts",
"default": "./dist/test-support/index.js"
},
"./*": {
"types": "./declarations/*.d.ts",
"default": "./dist/*.js"
Expand Down Expand Up @@ -56,7 +60,8 @@
"@glimmer/component": "^1.1.2"
},
"peerDependencies": {
"ember-source": "^4.8.0 || ^5.0.0"
"ember-source": "^4.8.0 || ^5.0.0",
"@ember/test-helpers": "^2.9.1 || ^3.0.0"
},
"devDependencies": {
"@babel/core": "^7.17.0",
Expand All @@ -65,6 +70,7 @@
"@babel/plugin-transform-class-static-block": "^7.20.0",
"@babel/preset-typescript": "^7.18.6",
"@babel/runtime": "^7.17.0",
"@ember/test-helpers": "^3.3.0",
"@embroider/addon-dev": "^4.1.0",
"@glimmer/interfaces": "^0.84.3",
"@glimmer/runtime": "^0.84.3",
Expand Down
1 change: 1 addition & 0 deletions ember-provide-consume-context/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export default {
'index.js',
'template-registry.js',
'context-registry.js',
'test-support/**/*.js',
]),

// These are the modules that should get reexported into the traditional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ interface Contexts {
}

interface ContextEntry {
// instance is an instance of a Glimmer component
// instance is an instance of a Glimmer component, or a "mock provider" from test-support helpers
instance: any;
// the property to read from the provider instance
key: string;
Expand All @@ -54,6 +54,10 @@ export class ProvideConsumeContextContainer {
// component instance.
contexts = new WeakMap<any, Contexts>();

// Global contexts are registered by test-support helpers to allow easily
// providing context values in tests.
#globalContexts: Contexts | null = null;

begin(): void {
this.reset();
}
Expand All @@ -70,6 +74,34 @@ export class ProvideConsumeContextContainer {
}
}

registerMockProvider = <
T extends keyof ContextRegistry,
U extends ContextRegistry[T],
>(
name: T,
value: U,
) => {
const mockProviderContext = {
instance: {
get value() {
return value;
},
},
key: 'value',
};

if (this.#globalContexts?.[name] != null) {
console.warn(
`A context provider with name "${name}" is already defined, and will be overwritten.`,
);
}

this.#globalContexts = {
...this.#globalContexts,
[name]: mockProviderContext,
};
};

enter(instance: ComponentInstance): void {
const actualComponentInstance = (instance?.state as any)?.component;

Expand Down Expand Up @@ -99,12 +131,18 @@ export class ProvideConsumeContextContainer {
const { current } = this;

let providerContexts: Contexts = {};

// If global contexts are defined, make sure providers can read them
if (this.#globalContexts != null) {
providerContexts = { ...this.#globalContexts };
}

if (this.contexts.has(current)) {
// If a provider is nested within another provider, we merge their
// contexts
const context = this.contexts.get(current);
if (context != null) {
providerContexts = { ...context };
providerContexts = { ...providerContexts, ...context };
}
}

Expand All @@ -128,12 +166,13 @@ export class ProvideConsumeContextContainer {
private registerComponent(component: any) {
const { current } = this;

// If a current context reference exists, register the component to it
if (this.contexts.has(current)) {
const globalContexts = this.#globalContexts ?? {};

// If a current context reference or global contexts exist, register them to the component
if (this.contexts.has(current) || Object.keys(globalContexts).length > 0) {
const context = this.contexts.get(current);
if (context != null) {
this.contexts.set(component, context);
}
const mergedContexts = { ...globalContexts, ...context };
this.contexts.set(component, mergedContexts);
}
}
}
57 changes: 57 additions & 0 deletions ember-provide-consume-context/src/test-support/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { getContext } from '@ember/test-helpers';
import type { TestContext } from '@ember/test-helpers';
import type { ProvideConsumeContextContainer } from '../-private/provide-consume-context-container';
import type ContextRegistry from '../context-registry';

export function setupRenderWrapper(templateFactory: object) {
const context = getContext() as TestContext | undefined;
if (context == null) {
throw new Error('Could not find test context');
}

if (context.owner == null) {
throw new Error('Could not find owner on test context');
}

const { owner } = context;

// Registers a custom outlet to use in the test, similar to how test-helpers does it:
// https://github.com/emberjs/ember-test-helpers/blob/9cec68dc6aa9c0a7a449eb89797eb81299fa727f/addon/addon-test-support/%40ember/test-helpers/setup-rendering-context.ts#L68
// Casting "as any" because "unregister" isn't defined on the Owner type, but it does exist.
(owner as any).unregister('template:-outlet');
owner.register('template:-outlet', templateFactory);
}

export function provide<
T extends keyof ContextRegistry,
U extends ContextRegistry[T],
>(name: T, value: U) {
const context = getContext() as TestContext | undefined;
if (context?.owner != null) {
const { owner } = context;

// https://github.com/emberjs/ember.js/blob/57073a0e9751d036d4bcfc11d5367e3f6ae751d2/packages/%40ember/-internals/glimmer/lib/renderer.ts#L284
// We cast to "any", because Renderer is a private API and isn't easily accessible.
// Even if we imported the type, "_runtime" is marked as private,
// so we wouldn't be able to access the current runtime or its type.
// If Context was implemented in Ember proper, it would have access to those private
// APIs, and this wouldn't look quite as illegal anymore.
const renderer = owner.lookup('renderer:-dom') as any;

if (renderer == null) {
throw new Error('Could not find "renderer:-dom" on owner');
}

const container = renderer._runtime?.env?.provideConsumeContextContainer as
| ProvideConsumeContextContainer
| undefined;

if (container == null) {
throw new Error(
'Could not find "provideConsumeContextContainer" instance in runtime environment',
);
}

container.registerMockProvider(name, value);
}
}
18 changes: 13 additions & 5 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions test-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@
"test:ember": "ember test"
},
"devDependencies": {
"ember-provide-consume-context": "*",
"@ember/optional-features": "^2.0.0",
"@ember/string": "^3.0.1",
"@ember/test-helpers": "^3.2.1",
"@ember/test-helpers": "^3.3.0",
"@embroider/test-setup": "^3.0.1",
"@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2",
Expand Down Expand Up @@ -78,6 +77,7 @@
"ember-load-initializers": "^2.1.2",
"ember-modifier": "^4.1.0",
"ember-page-title": "^7.0.0",
"ember-provide-consume-context": "*",
"ember-qunit": "^8.0.2",
"ember-resolver": "^10.0.0",
"ember-source": "~4.12.2",
Expand Down
Loading

0 comments on commit 611b1df

Please sign in to comment.