Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement testing RFC #1211

Merged
merged 23 commits into from
Apr 21, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface SettledState {
hasPendingLegacyWaiters: boolean;
hasPendingTestWaiters: boolean;
hasPendingRequests: boolean;
isRenderPending: boolean;
}

interface SummaryInfo {
Expand All @@ -39,6 +40,7 @@ interface SummaryInfo {
pendingScheduledQueueItemCount: Number;
pendingScheduledQueueItemStackTraces: (string | undefined)[];
hasRunLoop: boolean;
isRenderPending: boolean;
}

export default interface DebugInfo {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Ember from 'ember';
import {
macroCondition,
importSync,
dependencySatisfies,
} from '@embroider/macros';
import type { InternalComponentManager } from '@glimmer/interfaces';

let getComponentManager: (
definition: object,
owner: object
) => InternalComponentManager | null;

if (macroCondition(dependencySatisfies('ember-source', '>=3.27.0-alpha.1'))) {
let _getComponentManager =
//@ts-ignore
importSync('@glimmer/manager').getInternalComponentManager;

getComponentManager = (definition: object, owner: object) => {
return _getComponentManager(definition, true);
};
} else if (
macroCondition(dependencySatisfies('ember-source', '>=3.25.0-beta.1'))
) {
let _getComponentManager = (Ember as any).__loader.require(
'@glimmer/manager'
).getInternalComponentManager;

getComponentManager = (definition: object, owner: object) => {
return _getComponentManager(definition, true);
};
} else {
let _getComponentManager = (Ember as any).__loader.require(
'@glimmer/runtime'
).getComponentManager;

getComponentManager = (definition: object, owner: object) => {
return _getComponentManager(owner, definition);
};
}

export default getComponentManager;
25 changes: 25 additions & 0 deletions addon-test-support/@ember/test-helpers/-internal/is-component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { macroCondition, dependencySatisfies } from '@embroider/macros';

import getComponentManager from './get-component-manager';

/**
*
* @private
* @param {Object} maybeComponent The thing you think might be a component
* @param {Object} owner Owner, we need this for old versions of getComponentManager
* @returns {boolean} True if it's a component, false if not
*/
cafreeman marked this conversation as resolved.
Show resolved Hide resolved
function isComponent(maybeComponent: object, owner: object): boolean {
if (macroCondition(dependencySatisfies('ember-source', '>=3.25.0-beta.1'))) {
return !!getComponentManager(maybeComponent, owner);
} else {
return (
!!getComponentManager(maybeComponent, owner) ||
['@ember/component', '@ember/component/template-only'].includes(
maybeComponent.toString()
)
);
}
}

export default isComponent;
31 changes: 31 additions & 0 deletions addon-test-support/@ember/test-helpers/-internal/render-settled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Ember from 'ember';
import {
macroCondition,
importSync,
dependencySatisfies,
} from '@embroider/macros';

let renderSettled: () => Promise<void>;

// TODO need to add the actual version `@ember/renderer` landed once we know it
if (macroCondition(dependencySatisfies('ember-source', '>=4.9999999.0'))) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😆 🤣 😢

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, now that the Ember change has landed for realz let's update this to 4.5.0-beta.1.

//@ts-ignore
renderSettled = importSync('@ember/renderer').renderSettled;
} else if (
macroCondition(dependencySatisfies('ember-source', '>=3.27.0-alpha.1'))
) {
//@ts-ignore
renderSettled = importSync('@ember/-internals/glimmer').renderSettled;
} else if (
macroCondition(dependencySatisfies('ember-source', '>=3.6.0-alpha.1'))
) {
renderSettled = (Ember as any).__loader.require(
'@ember/-internals/glimmer'
).renderSettled;
} else {
renderSettled = (Ember as any).__loader.require(
'ember-glimmer'
).renderSettled;
}

export default renderSettled;
1 change: 1 addition & 0 deletions addon-test-support/@ember/test-helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {
render,
clearRender,
} from './setup-rendering-context';
export { default as rerender } from './rerender';
export {
default as setupApplicationContext,
visit,
Expand Down
13 changes: 13 additions & 0 deletions addon-test-support/@ember/test-helpers/rerender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import renderSettled from './-internal/render-settled';

/**
Returns a promise which will resolve when rendering has settled. Settled in
this context is defined as when all of the tags in use are "current" (e.g.
`renderers.every(r => r._isValid())`). When this is checked at the _end_ of
the run loop, this essentially guarantees that all rendering is completed.
cafreeman marked this conversation as resolved.
Show resolved Hide resolved
@public
@returns {Promise<void>} a promise which fulfills when rendering has settled
cafreeman marked this conversation as resolved.
Show resolved Hide resolved
*/
export default function rerender() {
return renderSettled();
}
10 changes: 9 additions & 1 deletion addon-test-support/@ember/test-helpers/settled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export interface SettledState {
hasPendingWaiters: boolean;
hasPendingRequests: boolean;
hasPendingTransitions: boolean | null;
isRenderPending: boolean;
pendingRequestCount: number;
debugInfo?: DebugInfo;
}
Expand Down Expand Up @@ -226,20 +227,25 @@ export function getSettledState(): SettledState {
let hasPendingTestWaiters = hasPendingWaiters();
let pendingRequestCount = pendingRequests();
let hasPendingRequests = pendingRequestCount > 0;
// TODO: Ideally we'd have a function in Ember itself that can synchronously identify whether
// or not there are any pending render operations, but this will have to suffice for now
let isRenderPending = !!hasRunLoop;
cafreeman marked this conversation as resolved.
Show resolved Hide resolved

return {
hasPendingTimers,
hasRunLoop,
hasPendingWaiters: hasPendingLegacyWaiters || hasPendingTestWaiters,
hasPendingRequests,
hasPendingTransitions: hasPendingTransitions(),
isRenderPending,
pendingRequestCount,
debugInfo: new TestDebugInfo({
hasPendingTimers,
hasRunLoop,
hasPendingLegacyWaiters,
hasPendingTestWaiters,
hasPendingRequests,
isRenderPending,
}),
};
}
Expand All @@ -261,14 +267,16 @@ export function isSettled(): boolean {
hasPendingRequests,
hasPendingWaiters,
hasPendingTransitions,
isRenderPending,
} = getSettledState();

if (
hasPendingTimers ||
hasRunLoop ||
hasPendingRequests ||
hasPendingWaiters ||
hasPendingTransitions
hasPendingTransitions ||
isRenderPending
) {
return false;
}
Expand Down
5 changes: 5 additions & 0 deletions addon-test-support/@ember/test-helpers/setup-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import {
Warning,
} from './-internal/warnings';

export const CALLED_SET = '__INTERNAL__called_set';
export const CALLED_SET_PROPERTIES = '__INTERNAL__called_set_properties';
rwjblue marked this conversation as resolved.
Show resolved Hide resolved

// This handler exists to provide the underlying data to enable the following methods:
// * getDeprecations()
// * getDeprecationsDuringCallback()
Expand Down Expand Up @@ -414,6 +417,7 @@ export default function setupContext(
enumerable: true,
value(key: string, value: any): any {
let ret = run(function () {
set(context, CALLED_SET, true);
rwjblue marked this conversation as resolved.
Show resolved Hide resolved
return set(context, key, value);
});

Expand All @@ -427,6 +431,7 @@ export default function setupContext(
enumerable: true,
value(hash: { [key: string]: any }): { [key: string]: any } {
let ret = run(function () {
set(context, CALLED_SET_PROPERTIES, true);
return setProperties(context, hash);
});

Expand Down
97 changes: 90 additions & 7 deletions addon-test-support/@ember/test-helpers/setup-rendering-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@ import getTestMetadata, { ITestMetadata } from './test-metadata';
import { deprecate } from '@ember/debug';
import { runHooks } from './-internal/helper-hooks';
import hasEmberVersion from './has-ember-version';
import isComponent from './-internal/is-component';
import { macroCondition, dependencySatisfies } from '@embroider/macros';
import { warn } from '@ember/debug';
import { CALLED_SET, CALLED_SET_PROPERTIES } from './setup-context';
import type { ComponentInstance } from '@glimmer/interfaces';

const OUTLET_TEMPLATE = hbs`{{outlet}}`;
const EMPTY_TEMPLATE = hbs``;
const INVOKE_PROVIDED_COMPONENT = hbs`<this.ProvidedComponent />`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chriskrycho here is this mystic this.ProvidedComponent that fails ember-optimized scenario


export interface RenderingTestContext extends TestContext {
render(template: TemplateFactory): Promise<void>;
Expand Down Expand Up @@ -84,17 +90,17 @@ export interface RenderOptions {
Renders the provided template and appends it to the DOM.

@public
@param {CompiledTemplate} template the template to render
@param {CompiledTemplate} templateOrComponent the template to render
cafreeman marked this conversation as resolved.
Show resolved Hide resolved
@param {RenderOptions} options options hash containing engine owner ({ owner: engineOwner })
@returns {Promise<void>} resolves when settled
*/
export function render(
template: TemplateFactory,
templateOrComponent: TemplateFactory | ComponentInstance,
options?: RenderOptions
): Promise<void> {
let context = getContext();

if (!template) {
if (!templateOrComponent) {
throw new Error('you must pass a template to `render()`');
}

Expand All @@ -115,9 +121,86 @@ export function render(
let OutletTemplate = lookupOutletTemplate(owner);
let ownerToRenderFrom = options?.owner || owner;

templateId += 1;
let templateFullName = `template:-undertest-${templateId}`;
ownerToRenderFrom.register(templateFullName, template);
if (macroCondition(dependencySatisfies('ember-source', '<3.24.0'))) {
// Pre 3.24, we just don't support rendering components at all, so we error
// if we find anything that isn't a template.
const isTemplate =
(Object.prototype.hasOwnProperty.call(templateOrComponent, '__id') &&
Object.prototype.hasOwnProperty.call(
templateOrComponent,
'__meta'
)) ||
(Object.prototype.hasOwnProperty.call(templateOrComponent, 'id') &&
Object.prototype.hasOwnProperty.call(templateOrComponent, 'meta'));
cafreeman marked this conversation as resolved.
Show resolved Hide resolved

if (!isTemplate) {
throw new Error(
'Prior to Ember 3.24, you may only pass templates to `render`.'
cafreeman marked this conversation as resolved.
Show resolved Hide resolved
);
}

templateId += 1;
let templateFullName = `template:-undertest-${templateId}`;
ownerToRenderFrom.register(templateFullName, templateOrComponent);
templateOrComponent = lookupTemplate(
ownerToRenderFrom,
templateFullName
);
} else {
if (isComponent(templateOrComponent, owner)) {
if (context.get(CALLED_SET)) {
warn(
"Calling `this.set` when rendering a component is an epic troll. Probably don't do it.",
{ id: 'set-warning' }
);
}

if (context.get(CALLED_SET_PROPERTIES)) {
warn(
"Calling `this.setProperties` when rendering a component is an epic troll. Probably don't do it.",
{
id: 'set-properties-warning',
}
);
}
rwjblue marked this conversation as resolved.
Show resolved Hide resolved

if (
macroCondition(
dependencySatisfies('ember-source', '>=3.25.0-beta.1')
)
) {
// In 3.25+, we can treat components as one big object and just pass them around/invoke them
// wherever, so we just assign the component to the `ProvidedComponent` property and invoke it
// in the test's template
context = {
ProvidedComponent: templateOrComponent,
};
templateOrComponent = INVOKE_PROVIDED_COMPONENT;
} else {
// Below 3.25, however, we *cannot* treat components as one big object and instead have to
// register their class and template independently and then invoke them with the `component`
// helper so they can actually be found by the resolver and rendered
templateId += 1;
let name = `-undertest-${templateId}`;
let componentFullName = `component:${name}`;
let templateFullName = `template:components/${name}`;
context = {
ProvidedComponent: name,
};
ownerToRenderFrom.register(componentFullName, templateOrComponent);
templateOrComponent = hbs`{{component this.ProvidedComponent}}`;
cafreeman marked this conversation as resolved.
Show resolved Hide resolved
ownerToRenderFrom.register(templateFullName, templateOrComponent);
}
} else {
templateId += 1;
let templateFullName = `template:-undertest-${templateId}`;
ownerToRenderFrom.register(templateFullName, templateOrComponent);
templateOrComponent = lookupTemplate(
ownerToRenderFrom,
templateFullName
);
}
}

let outletState = {
render: {
Expand All @@ -139,7 +222,7 @@ export function render(
name: 'index',
controller: context,
ViewClass: undefined,
template: lookupTemplate(ownerToRenderFrom, templateFullName),
template: templateOrComponent,
outlets: {},
},
outlets: {},
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@
"test": "ember test",
"test:all": "ember try:each"
},
"peerDependencies": {
"ember-source": "*"
cafreeman marked this conversation as resolved.
Show resolved Hide resolved
},
rwjblue marked this conversation as resolved.
Show resolved Hide resolved
"dependencies": {
"@ember/test-waiters": "^3.0.0",
"@embroider/macros": "^1.5.0",
rwjblue marked this conversation as resolved.
Show resolved Hide resolved
"broccoli-debug": "^0.6.5",
"broccoli-funnel": "^3.0.8",
"ember-cli-babel": "^7.26.6",
Expand All @@ -48,6 +52,9 @@
"@babel/cli": "^7.14.8",
"@babel/preset-typescript": "^7.15.0",
"@ember/optional-features": "^2.0.0",
"@glimmer/component": "^1.0.4",
"@glimmer/interfaces": "^0.84.1",
"@glimmer/reference": "^0.84.1",
Comment on lines +55 to +57
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

someday we'll detangle that circular reference and then not need all these extra packages 😭

"@types/ember": "^3.16.5",
"@types/ember-testing-helpers": "^0.0.4",
"@types/rsvp": "^4.0.4",
Expand Down