2023-10-24 22:29:29 +02:00
|
|
|
/**
|
|
|
|
|
* Copyright (c) Microsoft Corporation.
|
|
|
|
|
*
|
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
|
*
|
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
|
*
|
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
|
* limitations under the License.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// @ts-check
|
|
|
|
|
// This file is injected into the registry as text, no dependencies are allowed.
|
|
|
|
|
|
|
|
|
|
import 'zone.js';
|
2023-11-13 23:12:18 +01:00
|
|
|
import {
|
|
|
|
|
Component as defineComponent,
|
|
|
|
|
reflectComponentType
|
|
|
|
|
} from '@angular/core';
|
2023-10-24 22:29:29 +02:00
|
|
|
import { getTestBed, TestBed } from '@angular/core/testing';
|
2023-11-13 23:12:18 +01:00
|
|
|
import {
|
|
|
|
|
BrowserDynamicTestingModule,
|
|
|
|
|
platformBrowserDynamicTesting,
|
|
|
|
|
} from '@angular/platform-browser-dynamic/testing';
|
2023-10-24 22:29:29 +02:00
|
|
|
import { Router } from '@angular/router';
|
|
|
|
|
|
|
|
|
|
/** @typedef {import('@playwright/experimental-ct-core/types/component').Component} Component */
|
|
|
|
|
/** @typedef {import('@playwright/experimental-ct-core/types/component').JsxComponent} JsxComponent */
|
|
|
|
|
/** @typedef {import('@playwright/experimental-ct-core/types/component').ObjectComponent} ObjectComponent */
|
|
|
|
|
/** @typedef {import('@angular/core').Type} FrameworkComponent */
|
|
|
|
|
|
|
|
|
|
/** @type {Map<string, () => Promise<FrameworkComponent>>} */
|
|
|
|
|
const __pwLoaderRegistry = new Map();
|
|
|
|
|
/** @type {Map<string, FrameworkComponent>} */
|
|
|
|
|
const __pwRegistry = new Map();
|
|
|
|
|
/** @type {Map<string, import('@angular/core/testing').ComponentFixture>} */
|
|
|
|
|
const __pwFixtureRegistry = new Map();
|
2023-11-13 23:12:18 +01:00
|
|
|
/** @type {WeakMap<import('@angular/core/testing').ComponentFixture, Record<string, import('rxjs').Subscription>>} */
|
|
|
|
|
const __pwOutputSubscriptionRegistry = new WeakMap();
|
2023-10-24 22:29:29 +02:00
|
|
|
|
|
|
|
|
getTestBed().initTestEnvironment(
|
|
|
|
|
BrowserDynamicTestingModule,
|
|
|
|
|
platformBrowserDynamicTesting(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {{[key: string]: () => Promise<FrameworkComponent>}} components
|
|
|
|
|
*/
|
|
|
|
|
export function pwRegister(components) {
|
|
|
|
|
for (const [name, value] of Object.entries(components))
|
|
|
|
|
__pwLoaderRegistry.set(name, value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Component} component
|
|
|
|
|
* @returns {component is JsxComponent | ObjectComponent}
|
|
|
|
|
*/
|
|
|
|
|
function isComponent(component) {
|
|
|
|
|
return !(typeof component !== 'object' || Array.isArray(component));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Component} component
|
|
|
|
|
*/
|
|
|
|
|
async function __pwResolveComponent(component) {
|
|
|
|
|
if (!isComponent(component))
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
let componentFactory = __pwLoaderRegistry.get(component.type);
|
|
|
|
|
if (!componentFactory) {
|
|
|
|
|
// Lookup by shorthand.
|
|
|
|
|
for (const [name, value] of __pwLoaderRegistry) {
|
|
|
|
|
if (component.type.endsWith(`_${name}`)) {
|
|
|
|
|
componentFactory = value;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!componentFactory && component.type[0].toUpperCase() === component.type[0])
|
|
|
|
|
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`);
|
|
|
|
|
|
|
|
|
|
if(componentFactory)
|
|
|
|
|
__pwRegistry.set(component.type, await componentFactory())
|
|
|
|
|
|
|
|
|
|
if ('children' in component)
|
|
|
|
|
await Promise.all(component.children.map(child => __pwResolveComponent(child)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {import('@angular/core/testing').ComponentFixture} fixture
|
|
|
|
|
*/
|
|
|
|
|
function __pwUpdateProps(fixture, props = {}) {
|
|
|
|
|
for (const [name, value] of Object.entries(props))
|
|
|
|
|
fixture.debugElement.children[0].context[name] = value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {import('@angular/core/testing').ComponentFixture} fixture
|
|
|
|
|
*/
|
|
|
|
|
function __pwUpdateEvents(fixture, events = {}) {
|
2023-11-13 23:12:18 +01:00
|
|
|
const outputSubscriptionRecord =
|
|
|
|
|
__pwOutputSubscriptionRegistry.get(fixture) ?? {};
|
|
|
|
|
for (const [name, listener] of Object.entries(events)) {
|
|
|
|
|
/* Unsubscribe previous listener. */
|
|
|
|
|
outputSubscriptionRecord[name]?.unsubscribe();
|
|
|
|
|
|
|
|
|
|
const subscription = fixture.debugElement.children[0].componentInstance[
|
|
|
|
|
name
|
|
|
|
|
].subscribe((event) => listener(event));
|
|
|
|
|
|
|
|
|
|
/* Store new subscription. */
|
|
|
|
|
outputSubscriptionRecord[name] = subscription;
|
2023-10-24 22:29:29 +02:00
|
|
|
}
|
2023-11-13 23:12:18 +01:00
|
|
|
|
|
|
|
|
/* Update output subscription registry. */
|
|
|
|
|
__pwOutputSubscriptionRegistry.set(fixture, outputSubscriptionRecord);
|
2023-10-24 22:29:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function __pwUpdateSlots(Component, slots = {}, tagName) {
|
|
|
|
|
const wrapper = document.createElement(tagName);
|
|
|
|
|
for (const [key, value] of Object.entries(slots)) {
|
|
|
|
|
let slotElements;
|
|
|
|
|
if (typeof value !== 'object')
|
|
|
|
|
slotElements = [__pwCreateSlot(value)];
|
|
|
|
|
|
|
|
|
|
if (Array.isArray(value))
|
|
|
|
|
slotElements = value.map(__pwCreateSlot);
|
|
|
|
|
|
|
|
|
|
if (!slotElements)
|
|
|
|
|
throw new Error(`Invalid slot with name: \`${key}\` supplied to \`mount()\``);
|
|
|
|
|
|
|
|
|
|
for (const slotElement of slotElements) {
|
|
|
|
|
if (!slotElement)
|
|
|
|
|
throw new Error(`Invalid slot with name: \`${key}\` supplied to \`mount()\``);
|
|
|
|
|
|
|
|
|
|
if (key === 'default') {
|
|
|
|
|
wrapper.appendChild(slotElement);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (slotElement.nodeName === '#text') {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Invalid slot with name: \`${key}\` supplied to \`mount()\`, expected \`HTMLElement\` but received \`TextNode\`.`
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slotElement.setAttribute(key, '');
|
|
|
|
|
wrapper.appendChild(slotElement);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
TestBed.overrideTemplate(Component, wrapper.outerHTML);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {any} value
|
|
|
|
|
* @return {?HTMLElement}
|
|
|
|
|
*/
|
|
|
|
|
function __pwCreateSlot(value) {
|
|
|
|
|
return /** @type {?HTMLElement} */ (
|
|
|
|
|
document
|
|
|
|
|
.createRange()
|
|
|
|
|
.createContextualFragment(value)
|
|
|
|
|
.firstChild
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @param {Component} component
|
|
|
|
|
*/
|
|
|
|
|
async function __pwRenderComponent(component) {
|
|
|
|
|
const Component = __pwRegistry.get(component.type);
|
|
|
|
|
if (!Component)
|
|
|
|
|
throw new Error(`Unregistered component: ${component.type}. Following components are registered: ${[...__pwRegistry.keys()]}`);
|
|
|
|
|
|
|
|
|
|
if (component.kind !== 'object')
|
|
|
|
|
throw new Error('JSX mount notation is not supported');
|
|
|
|
|
|
|
|
|
|
const componentMetadata = reflectComponentType(Component);
|
|
|
|
|
if (!componentMetadata?.isStandalone)
|
|
|
|
|
throw new Error('Only standalone components are supported');
|
|
|
|
|
|
|
|
|
|
const WrapperComponent = defineComponent({
|
|
|
|
|
selector: 'pw-wrapper-component',
|
|
|
|
|
template: ``,
|
|
|
|
|
})(class {});
|
|
|
|
|
|
|
|
|
|
TestBed.configureTestingModule({
|
|
|
|
|
imports: [Component],
|
|
|
|
|
declarations: [WrapperComponent]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await TestBed.compileComponents();
|
|
|
|
|
|
|
|
|
|
__pwUpdateSlots(WrapperComponent, component.options?.slots, componentMetadata.selector);
|
|
|
|
|
|
|
|
|
|
// TODO: only inject when router is provided
|
|
|
|
|
TestBed.inject(Router).initialNavigation();
|
|
|
|
|
|
|
|
|
|
const fixture = TestBed.createComponent(WrapperComponent);
|
|
|
|
|
fixture.nativeElement.id = 'root';
|
|
|
|
|
|
|
|
|
|
__pwUpdateProps(fixture, component.options?.props);
|
|
|
|
|
__pwUpdateEvents(fixture, component.options?.on);
|
|
|
|
|
|
|
|
|
|
fixture.autoDetectChanges();
|
|
|
|
|
|
|
|
|
|
return fixture;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
window.playwrightMount = async (component, rootElement, hooksConfig) => {
|
|
|
|
|
await __pwResolveComponent(component);
|
|
|
|
|
for (const hook of window.__pw_hooks_before_mount || [])
|
|
|
|
|
await hook({ hooksConfig, TestBed });
|
|
|
|
|
|
|
|
|
|
const fixture = await __pwRenderComponent(component);
|
|
|
|
|
|
|
|
|
|
for (const hook of window.__pw_hooks_after_mount || [])
|
|
|
|
|
await hook({ hooksConfig });
|
|
|
|
|
|
|
|
|
|
__pwFixtureRegistry.set(rootElement.id, fixture);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.playwrightUnmount = async rootElement => {
|
|
|
|
|
const fixture = __pwFixtureRegistry.get(rootElement.id);
|
2023-11-13 23:12:18 +01:00
|
|
|
if (!fixture) throw new Error('Component was not mounted');
|
|
|
|
|
|
|
|
|
|
/* Unsubscribe from all outputs. */
|
|
|
|
|
for (const subscription of Object.values(__pwOutputSubscriptionRegistry.get(fixture) ?? {}))
|
|
|
|
|
subscription?.unsubscribe();
|
|
|
|
|
__pwOutputSubscriptionRegistry.delete(fixture);
|
2023-10-24 22:29:29 +02:00
|
|
|
|
|
|
|
|
fixture.destroy();
|
|
|
|
|
fixture.nativeElement.replaceChildren();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.playwrightUpdate = async (rootElement, component) => {
|
|
|
|
|
await __pwResolveComponent(component);
|
|
|
|
|
if (component.kind === 'jsx')
|
|
|
|
|
throw new Error('JSX mount notation is not supported');
|
|
|
|
|
|
|
|
|
|
if (component.options?.slots)
|
|
|
|
|
throw new Error('Update slots is not supported yet');
|
|
|
|
|
|
|
|
|
|
const fixture = __pwFixtureRegistry.get(rootElement.id);
|
|
|
|
|
if (!fixture)
|
|
|
|
|
throw new Error('Component was not mounted');
|
|
|
|
|
|
|
|
|
|
__pwUpdateProps(fixture, component.options?.props);
|
|
|
|
|
__pwUpdateEvents(fixture, component.options?.on);
|
|
|
|
|
|
|
|
|
|
fixture.detectChanges();
|
|
|
|
|
};
|