Skip to content
Merged
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ log.txt
.DS_Store
.idea/
.vscode/
.claude
.history/
.sass-cache/
.versions/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ export const lazyComponentTransform = (
return updateLazyComponentClass(transformOpts, styleStatements, node, moduleFile, cmp, buildCtx);
} else if (module?.isMixin) {
return updateMixin(node, moduleFile, cmp, transformOpts);
} else if (buildCtx.config._isTesting && buildCtx.config.flags.spec) {
} else if (buildCtx.config._isTesting && buildCtx.config.flags.spec && !buildCtx.config.flags.e2e) {
// because (during spec tests) *only* the component class is added as a module
// let's tidy up all class nodes in testing mode
// let's tidy up all class nodes in testing mode, but only when running spec tests alone
// (not when running both spec and e2e, as e2e builds will handle transformation differently)
return updateConstructor(node, Array.from(node.members), [], []);
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/mock-doc/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,12 @@ export class MockWindow {
scrollY: number;

// event handlers
CustomEvent: typeof MockCustomEvent;
Event: typeof MockEvent;
Headers: typeof MockHeaders;
FocusEvent: typeof MockFocusEvent;
KeyboardEvent: typeof MockKeyboardEvent;
MouseEvent: typeof MockMouseEvent;
declare CustomEvent: typeof MockCustomEvent;
declare Event: typeof MockEvent;
declare Headers: typeof MockHeaders;
declare FocusEvent: typeof MockFocusEvent;
declare KeyboardEvent: typeof MockKeyboardEvent;
declare MouseEvent: typeof MockMouseEvent;

constructor(html: string | boolean = null) {
if (html !== false) {
Expand Down
33 changes: 32 additions & 1 deletion src/runtime/test/mixin.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, h, MixedInCtor, Mixin, Prop, State } from '@stencil/core';
import { Component, Event, EventEmitter, h, MixedInCtor, Mixin, Prop, State } from '@stencil/core';
import { newSpecPage } from '@stencil/core/testing';

describe('mixin', () => {
Expand Down Expand Up @@ -63,4 +63,35 @@ describe('mixin', () => {
</mixin-test>
`);
});

it('can initialize with an @Event', async () => {
const MyMixin = <B extends MixedInCtor>(Base: B) => {
class Test extends Base {
public emitTest() {
(this as any).test.emit();
}
}
return Test;
};

@Component({
tag: 'mixin-test',
})
class MixinTest extends Mixin(MyMixin) {
@Event() test: EventEmitter;

componentDidLoad() {
this.emitTest();
}
}

const { root } = await newSpecPage({
components: [MixinTest],
html: `<mixin-test></mixin-test>`,
});

expect(root).toEqualHtml(`
<mixin-test></mixin-test>
`);
});
});
17 changes: 10 additions & 7 deletions src/testing/jest/jest-27-and-under/jest-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@ import { JestPuppeteerEnvironmentConstructor } from '../jest-apis';

export function createJestPuppeteerEnvironment(): JestPuppeteerEnvironmentConstructor {
const JestEnvironment = class extends NodeEnvironment {
// TODO(STENCIL-1023): Remove this @ts-expect-error
// @ts-expect-error - Stencil's Jest environment adds additional properties to the Jest global, but does not extend it
global: JestEnvironmentGlobal;
browser: any = null;
pages: any[] = [];
browser: any;
pages: any[];

constructor(config: any) {
super(config);
// Initialize fields after super() to ensure parent's global is not overwritten
// (required for ES2022+ target where field initializers run after super())
this.browser = null;
this.pages = [];
// Debug: Verify this.global is properly set
}

override async setup() {
if ((process.env as E2EProcessEnv).__STENCIL_E2E_TESTS__ === 'true') {
this.global.__NEW_TEST_PAGE__ = this.newPuppeteerPage.bind(this);
this.global.__CLOSE_OPEN_PAGES__ = this.closeOpenPages.bind(this);
const globalContext = this.global as unknown as JestEnvironmentGlobal;
globalContext.__NEW_TEST_PAGE__ = this.newPuppeteerPage.bind(this);
globalContext.__CLOSE_OPEN_PAGES__ = this.closeOpenPages.bind(this);
}
}

Expand Down
21 changes: 12 additions & 9 deletions src/testing/jest/jest-28/jest-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,24 @@ import { JestPuppeteerEnvironmentConstructor } from '../jest-apis';

export function createJestPuppeteerEnvironment(): JestPuppeteerEnvironmentConstructor {
const JestEnvironment = class extends NodeEnvironment {
// TODO(STENCIL-1023): Remove this @ts-expect-error
// @ts-expect-error - Stencil's Jest environment adds additional properties to the Jest global, but does not extend it
global: JestEnvironmentGlobal;
browser: any = null;
pages: any[] = [];
testPath: string | null = null;
browser: any;
pages: any[];
testPath: string | null;

constructor(config: any, context: any) {
super(config, context);
// Initialize fields after super() to ensure parent's global is not overwritten
// (required for ES2022+ target where field initializers run after super())
this.browser = null;
this.pages = [];
this.testPath = context.testPath;
}

override async setup() {
if ((process.env as E2EProcessEnv).__STENCIL_E2E_TESTS__ === 'true') {
this.global.__NEW_TEST_PAGE__ = this.newPuppeteerPage.bind(this);
this.global.__CLOSE_OPEN_PAGES__ = this.closeOpenPages.bind(this);
const globalContext = this.global as unknown as JestEnvironmentGlobal;
globalContext.__NEW_TEST_PAGE__ = this.newPuppeteerPage.bind(this);
globalContext.__CLOSE_OPEN_PAGES__ = this.closeOpenPages.bind(this);
}
}

Expand Down Expand Up @@ -56,7 +58,8 @@ export function createJestPuppeteerEnvironment(): JestPuppeteerEnvironmentConstr
currentParent = currentParent.parent;
}
// Set the current spec for us to inspect for using the default reporter in screenshot tests.
this.global.currentSpec = {
const globalContext = this.global as unknown as JestEnvironmentGlobal;
globalContext.currentSpec = {
// the event's test's name is analogous to the original description in earlier versions of jest
description: eventTest.name,
fullName,
Expand Down
21 changes: 12 additions & 9 deletions src/testing/jest/jest-29/jest-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,24 @@ import { JestPuppeteerEnvironmentConstructor } from '../jest-apis';

export function createJestPuppeteerEnvironment(): JestPuppeteerEnvironmentConstructor {
const JestEnvironment = class extends NodeEnvironment {
// TODO(STENCIL-1023): Remove this @ts-expect-error
// @ts-expect-error - Stencil's Jest environment adds additional properties to the Jest global, but does not extend it
global: JestEnvironmentGlobal;
browser: any = null;
pages: any[] = [];
testPath: string | null = null;
browser: any;
pages: any[];
testPath: string | null;

constructor(config: any, context: any) {
super(config, context);
// Initialize fields after super() to ensure parent's global is not overwritten
// (required for ES2022+ target where field initializers run after super())
this.browser = null;
this.pages = [];
this.testPath = context.testPath;
}

override async setup() {
if ((process.env as E2EProcessEnv).__STENCIL_E2E_TESTS__ === 'true') {
this.global.__NEW_TEST_PAGE__ = this.newPuppeteerPage.bind(this);
this.global.__CLOSE_OPEN_PAGES__ = this.closeOpenPages.bind(this);
const globalContext = this.global as unknown as JestEnvironmentGlobal;
globalContext.__NEW_TEST_PAGE__ = this.newPuppeteerPage.bind(this);
globalContext.__CLOSE_OPEN_PAGES__ = this.closeOpenPages.bind(this);
}
}

Expand Down Expand Up @@ -56,7 +58,8 @@ export function createJestPuppeteerEnvironment(): JestPuppeteerEnvironmentConstr
currentParent = currentParent.parent;
}
// Set the current spec for us to inspect for using the default reporter in screenshot tests.
this.global.currentSpec = {
const globalContext = this.global as unknown as JestEnvironmentGlobal;
globalContext.currentSpec = {
// the event's test's name is analogous to the original description in earlier versions of jest
description: eventTest.name,
fullName,
Expand Down
50 changes: 50 additions & 0 deletions src/testing/platform/testing-host-ref.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type * as d from '@stencil/core/internal';
import { createEvent } from '../../runtime/event-emitter';
import { EVENT_FLAGS } from '@utils';

/**
* Retrieve the data structure tracking the component by its runtime reference
Expand Down Expand Up @@ -35,6 +37,54 @@ export const registerInstance = (lazyInstance: any, hostRef: d.HostRef | null |

lazyInstance.__stencil__getHostRef = () => hostRef;
hostRef.$lazyInstance$ = lazyInstance;

// Create EventEmitters for all events from the component and its parent classes/mixins
// This is necessary to support events defined in mixins that may not have been included
// in the component's compiled constructor
const Cstr = lazyInstance.constructor as d.ComponentTestingConstructor;

// Collect all events from the component and its prototype chain
const allEvents: d.ComponentCompilerEvent[] = [];
const seenEventMethods = new Set<string>();

// First, add events from the component's COMPILER_META
if (Cstr.COMPILER_META && Cstr.COMPILER_META.events) {
Cstr.COMPILER_META.events.forEach((event: d.ComponentCompilerEvent) => {
if (!seenEventMethods.has(event.method)) {
allEvents.push(event);
seenEventMethods.add(event.method);
}
});
}

// Then, walk the prototype chain to find events from parent classes/mixins
let currentProto = Object.getPrototypeOf(Cstr);
while (currentProto && currentProto !== Function.prototype && currentProto.name) {
// Check if this parent class has a static events getter
if (typeof currentProto.events === 'object' && Array.isArray(currentProto.events)) {
currentProto.events.forEach((event: d.ComponentCompilerEvent) => {
if (!seenEventMethods.has(event.method)) {
allEvents.push(event);
seenEventMethods.add(event.method);
}
});
}
currentProto = Object.getPrototypeOf(currentProto);
}

// Create EventEmitters for all collected events
allEvents.forEach((eventMeta: d.ComponentCompilerEvent) => {
// Only create the event emitter if it doesn't already exist on the instance
// (it might already exist if it was created by the compiled constructor)
if (!lazyInstance[eventMeta.method]) {
let flags = 0;
if (eventMeta.bubbles) flags |= EVENT_FLAGS.Bubbles;
if (eventMeta.composed) flags |= EVENT_FLAGS.Composed;
if (eventMeta.cancelable) flags |= EVENT_FLAGS.Cancellable;

lazyInstance[eventMeta.method] = createEvent(lazyInstance, eventMeta.name, flags);
}
});
};

/**
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"sourceMap": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"target": "es2018",
"target": "es2022",
"useUnknownInCatchVariables": true,
"types": ["jest", "node"]
},
Expand Down
Loading