How to access shadowDom when testing Lit element with open-wc - testing

Lit docs refer to Web Test Runner as testing. It navigates to this example page.
I tried testing MyElement, which has only one <p>.
import { LitElement, html } from "lit";
import { customElement } from "lit/decorators.js";
#customElement("my-element")
export class MyElement extends LitElement {
render() {
return html`<p>Hello, World.</p>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"my-element": MyElement;
}
}
When testing by open-wc, the element's shadowDom did not have <p> in descendant.
import { expect, fixture, html } from "#open-wc/testing";
import { MyElement } from "../src/MyElement";
it("get shadowDom", async () => {
const el: MyElement = await fixture(html`<my-element></my-element>`);
expect(el).shadowDom.to.be.not.null; // passed
expect(el).shadowDom.to.have.descendant("p"); // failed
});
Does it need more setup to test Lit elements with open-wc?
web-test-runner.config.js is:
import { esbuildPlugin } from '#web/dev-server-esbuild';
export default {
files: ['test/*.test.ts'],
plugins: [esbuildPlugin({ ts: true })],
};

Try shadowRoot instead of shadowDom:
it("get shadowDom", async () => {
const el = await fixture(
html` <my-element></my-element>`
);
const descendant = el.shadowRoot!.querySelector("p")!;
expect(descendant).to.be.not.null;
});

I had similar issue. In my case shadowRoot was "null". To have shadowRoot content I had to import my web component like that:
import './myWebcomponent';

Related

How to change the title of odoo in odoo15 without editing the source code

I want to change the title of odoo in the browser tag, but for some reason, I cannot change the source code.
I found the code where to set the title in webclient.js but I don't know how to use it in my code.
/** #odoo-module **/
import { ActionContainer } from "./actions/action_container";
import { NavBar } from "./navbar/navbar";
import { useBus, useEffect, useService } from "#web/core/utils/hooks";
import { useTooltip } from "#web/core/tooltip/tooltip_hook";
import { NotUpdatable } from "../core/utils/components";
import { MainComponentsContainer } from "../core/main_components_container";
import { useOwnDebugContext } from "../core/debug/debug_context";
import { registry } from "#web/core/registry";
import { DebugMenu } from "#web/core/debug/debug_menu";
import { localization } from "#web/core/l10n/localization";
const { Component, hooks } = owl;
const { useExternalListener } = hooks;
export class WebClient extends Component {
setup() {
this.menuService = useService("menu");
this.actionService = useService("action");
this.title = useService("title");
this.router = useService("router");
this.user = useService("user");
useService("legacy_service_provider");
useOwnDebugContext({ categories: ["default"] });
if (this.env.debug) {
registry.category("systray").add(
"web.debug_mode_menu",
{
Component: DebugMenu,
},
{ sequence: 100 }
);
}
this.localization = localization;
this.title.setParts({ zopenerp: "Odoo" }); // zopenerp is easy to grep
useBus(this.env.bus, "ROUTE_CHANGE", this.loadRouterState);
useBus(this.env.bus, "ACTION_MANAGER:UI-UPDATED", (mode) => {
if (mode !== "new") {
this.el.classList.toggle("o_fullscreen", mode === "fullscreen");
}
});
.......
You need to patch the web client component class method
when one wishes to patch a standard method from a class, then we actually need to patch the prototype
Example:
import { WebClient } from "#web/webclient/webclient"
import {patch} from "web.utils";
patch(WebClient.prototype, "title patch", {
setup() {
this._super();
this.title.setParts({ zopenerp: "" });
},
});

Vue test utils: extend VueWrapper to add plugin method

I am using Vue test utils and Typescript. I have added Data Test ID Plugin.
How can I extend VueWrapper interface to avoid this error:
Property 'findByTestId' does not exist on type 'VueWrapper<{ $: ComponentInternalInstance; $data: { showUserMenu: boolean ...
One solution is to export a type that adds findByTestId:
// my-vue-test-utils-plugin.ts
import { config, DOMWrapper, createWrapperError, type VueWrapper } from '#vue/test-utils'
👇
export type TestWrapper = VueWrapper<any> & {
findByTestId: (selector: string) => DOMWrapper<HTMLElement>
}
const DataTestIdPlugin = (wrapper: VueWrapper<any>) => {
function findByTestId(selector: string) {
const dataSelector = `[data-testid='${selector}']`
const element = wrapper.element.querySelector(dataSelector)
if (element) {
return new DOMWrapper(element)
}
return createWrapperError('DOMWrapper')
}
return {
findByTestId
}
}
config.plugins.VueWrapper.install(DataTestIdPlugin as any)
Then, use type assertion (as keyword followed by the exported type above) on the mount() result:
// MyComponent.spec.ts
import type { TestWrapper } from './my-vue-test-utils-plugin.ts'
describe('MyComponent', () => {
it('renders properly', () => { 👇
const wrapper = mount(MyComponent) as TestWrapper
expect(wrapper.findByTestId('my-component').text()).toBe('Hello World')
})
})
Another option is to create a .d.ts file e.g. vue-test-utils.d.ts with the following content:
import { DOMWrapper } from '#vue/test-utils';
declare module '#vue/test-utils' {
export class VueWrapper {
findByTestId(selector: string): DOMWrapper[];
}
}
So you're able to extend the existing definition of the VueWrapper class.

How do I create a dummy component to use in an Ember integration test?

I have a component some-container that takes a hash of ids mapped to other ember components. This is how it's used:
{{modules/some-container
pageKey="foo"
widgetIdToComponent=(hash
foo=(component "modules/dummy-component")
)
}}
I'm writing an integration test for this component and I want to keep the test independent of other components. Is there a way to define dummy components within an Ember integration test file?
The existing answer from #Tyler Becks is good for legacy ember-qunit, but with native qunit (since: 2017: https://github.com/emberjs/rfcs/blob/master/text/0232-simplify-qunit-testing-api.md), you'd want something like this (known to work with at least Ember Octane (3.16+)):
import { test, module } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { hbs } from 'ember-cli-htmlbars';
import { render } from '#ember/test-helpers';
import { setComponentTemplate } from '#ember/component';
import templateOnly from '#ember/component/template-only';
import Component from '#glimmer/component';
module('name of test suite', function(hooks) {
setupRenderingTest(hooks);
test('name of test', async function(assert) {
class MyComponent extends Component {}
setComponetTemplate(hbs`your template here`, MyComponent);
this.owner.register('component:component-name', MyComponent);
await render(hbs`<ComponentName />`);
});
// alternatively, things get a bit simpler if you're using
// ember-source 3.25+
test('name of test 2', async function(assert) {
class MyComponent extends Component {}
setComponetTemplate(hbs`your template here`, MyComponent);
this.setProperties({ MyComponent });
await render(hbs`<this.MyComponent />`);
});
// template-only components are even simpler still
test('name of test 3', async function(assert) {
this.setProperties({
MyComponent: setComponetTemplate(
hbs`your template here`,
templateOnly()
);
});
await render(hbs`<this.MyComponent />`);
});
});
Figured it out! The solution is to use this.register. See below:
moduleForComponent('some-container', 'Integration | Component | some-container', {
integration: true,
beforeEach() {
this.register(
'component:foo-div',
Component.extend({
layout: hbs`<div data-test-foo />`
})
);
this.register(
'component:bar-div',
Component.extend({
layout: hbs`<div data-test-bar />`
})
);
this.component = hbs`
{{modules/some-container
pageKey="foo"
widgetIdToComponent=(hash
fooId=(component "foo-div")
barId=(component "bar-div")
)
}}`;
}
});
test('it renders foo div when only foo div is returned', function(assert) {
this.render(this.component);
assert.dom('[data-test-foo]').exists('foo div renders');
assert.dom('[data-test-bar]').doesNotExist('foo div renders');
});

Open a VueJS component on a new window

I have a basic VueJS application with only one page.
It's not a SPA, and I do not use vue-router.
I would like to implement a button that when clicked executes the window.open() function with content from one of my Vue Components.
Looking at the documentation from window.open() I saw the following statement for URL:
URL accepts a path or URL to an HTML page, image file, or any other resource which is supported by the browser.
Is it possible to pass a component as an argument for window.open()?
I was able to use some insights from an article about Portals in React to create a Vue component which is able to mount its children in a new window, while preserving reactivity! It's as simple as:
<window-portal>
I appear in a new window!
</window-portal>
Try it in this codesandbox!
The code for this component is as follows:
<template>
<div v-if="open">
<slot />
</div>
</template>
<script>
export default {
name: 'window-portal',
props: {
open: {
type: Boolean,
default: false,
}
},
data() {
return {
windowRef: null,
}
},
watch: {
open(newOpen) {
if(newOpen) {
this.openPortal();
} else {
this.closePortal();
}
}
},
methods: {
openPortal() {
this.windowRef = window.open("", "", "width=600,height=400,left=200,top=200");
this.windowRef.addEventListener('beforeunload', this.closePortal);
// magic!
this.windowRef.document.body.appendChild(this.$el);
},
closePortal() {
if(this.windowRef) {
this.windowRef.close();
this.windowRef = null;
this.$emit('close');
}
}
},
mounted() {
if(this.open) {
this.openPortal();
}
},
beforeDestroy() {
if (this.windowRef) {
this.closePortal();
}
}
}
</script>
The key is the line this.windowRef.document.body.appendChild(this.$el); this line effectively removes the DOM element associated with the Vue component (the top-level <div>) from the parent window and inserts it into the body of the child window. Since this element is the same reference as the one Vue would normally update, just in a different place, everything Just Works - Vue continues to update the element in response to databinding changes, despite it being mounted in a new window. I was actually quite surprised at how simple this was!
You cannot pass a Vue component, because window.open doesn't know about Vue. What you can do, however, is to create a route which displays your component and pass this route's URL to window.open, giving you a new window with your component. Communication between the components in different windows might get tricky though.
For example, if your main vue is declared like so
var app = new Vue({...});
If you only need to render a few pieces of data in the new window, you could just reference the data model from the parent window.
var app1 = window.opener.app;
var title = app.title;
var h1 = document.createElement("H1");
h1.innerHTML = title;
document.body.appendChild(h1);
I ported the Alex contribution to Composition API and works pretty well.
The only annoyance is that the created window ignores size and position, maybe because it is launched from a Chrome application that is fullscreen. Any idea?
<script setup lang="ts">
import {ref, onMounted, onBeforeUnmount, watch, nextTick} from "vue";
const props = defineProps<{modelValue: boolean;}>();
const emit = defineEmits(["update:modelValue"]);
let windowRef: Window | null = null;
const portal = ref(null);
const copyStyles = (sourceDoc: Document, targetDoc: Document): void => {
// eslint-disable-next-line unicorn/prefer-spread
for(const styleSheet of Array.from(sourceDoc.styleSheets)) {
if(styleSheet.cssRules) {
// for <style> elements
const nwStyleElement = sourceDoc.createElement("style");
// eslint-disable-next-line unicorn/prefer-spread
for(const cssRule of Array.from(styleSheet.cssRules)) {
// write the text of each rule into the body of the style element
nwStyleElement.append(sourceDoc.createTextNode(cssRule.cssText));
}
targetDoc.head.append(nwStyleElement);
}
else if(styleSheet.href) {
// for <link> elements loading CSS from a URL
const nwLinkElement = sourceDoc.createElement("link");
nwLinkElement.rel = "stylesheet";
nwLinkElement.href = styleSheet.href;
targetDoc.head.append(nwLinkElement);
}
}
};
const openPortal = (): void => {
nextTick().then((): void => {
windowRef = window.open("", "", "width=600,height=400,left=200,top=200");
if(!windowRef || !portal.value) return;
windowRef.document.body.append(portal.value);
copyStyles(window.document, windowRef.document);
windowRef.addEventListener("beforeunload", closePortal);
})
.catch((error: Error) => console.error("Cannot instantiate portal", error.message));
};
const closePortal = (): void => {
if(windowRef) {
windowRef.close();
windowRef = null;
emit("update:modelValue", false);
}
};
watch(props, () => {
if(props.modelValue) {
openPortal();
}
else {
closePortal();
}
});
onMounted(() => {
if(props.modelValue) {
openPortal();
}
});
onBeforeUnmount(() => {
if(windowRef) {
closePortal();
}
});
</script>
<template>
<div v-if="props.modelValue" ref="portal">
<slot />
</div>
</template>

Inheritance of Angular 5 components with overriding the decorator properties

In Angular 2/4 we could create custom decorator for extending parent component. Actual overriding of the decorator properties was handled as needed in the custom decorator. To get parent annotations we used:
let parentAnnotations = Reflect.getMetadata('annotations', parentTarget);
After update to Angular 5 this doesn't work anymore. Regarding this
answer we could use:
target['__annotations__'][0] for getting parent component annotations.
In order to set annotations in the current component in Angular 2/4 we used:
let metadata = new Component(annotation);
Reflect.defineMetadata('annotations', [ metadata ], target);
How can set current component annotations in Angular 5?
At the end I came up to this implementation of a custom decorator (extendedcomponent.decorator.ts):
import { Component } from '#angular/core';
export function ExtendedComponent(extendedConfig: Component = {}) {
return function (target: Function) {
const ANNOTATIONS = '__annotations__';
const PARAMETERS = '__paramaters__';
const PROP_METADATA = '__prop__metadata__';
const annotations = target[ANNOTATIONS] || [];
const parameters = target[PARAMETERS] || [];
const propMetadata = target[PROP_METADATA] || [];
if (annotations.length > 0) {
const parentAnnotations = Object.assign({}, annotations[0]);
Object.keys(parentAnnotations).forEach(key => {
if (parentAnnotations.hasOwnProperty(key)) {
if (!extendedConfig.hasOwnProperty(key)) {
extendedConfig[key] = parentAnnotations[key];
annotations[0][key] = '';
} else {
if (extendedConfig[key] === parentAnnotations[key]){
annotations[0][key] = '';
}
}
}
});
}
return Component(extendedConfig)(target);
};
}
Example usage:
First implement the parent component as usual (myparent.component.ts):
import { Component, Output, EventEmitter, Input } from '#angular/core';
#Component({
selector: 'my-component',
templateUrl: 'my.component.html'
})
export class MyParentComponent implements OnInit {
#Input() someInput: Array<any>;
#Output() onChange: EventEmitter<any> = new EventEmitter();
constructor(
public formatting: FormattingService
) {
}
ngOnInit() {
}
onClick() {
this.onChange.emit();
}
}
After that implement child component which inherit the parent component:
import { Component, OnInit } from '#angular/core';
import { ExtendedComponent } from './extendedcomponent.decorator';
import { MyParentComponent } from './myparent.component';
#ExtendedComponent ({
templateUrl: 'mychild.component.html'
})
export class MyChildComponent extends MyParentComponent {
}
Note: This is not officially documented and may not work in many cases. I hope that it will help somebody else, but use it at your own risk.