LWC OSS & Jest - Cannot create elements to test as createElement no longer exists - lwc

I'm trying to write a unit test for my open source LWC but cannot complete the test as the createElement member from the LWC package no longer exists. I have been following the docs on lwc.dev but they seem to be outdated and still using createElement in their example.
I have a TypeScript-enabled LWR application and I have installed jest, jest-environment-jsdom, #types/jest and #lwc/jest-preset.
Here is my component:
// hello.html
<template>
<h1>Hello, {greeting}!<h1>
<template>
// hello.ts
import { LightningElement } from 'lwc';
export default class Hello extends LightningElement {
greeting = 'World';
}
And here is my unit test:
// __tests__/hello.test.ts
import { createElement } from 'lwc';
import Hello from '../hello';
describe('hello', () => {
afterEach(() => {
while (document.body.firstChild) {
document.body.removeChild(document.body.firstChild);
}
});
it('renders greeting', () => {
const element = createElement('tut-hello', {
is: Hello
});
document.body.appendChild(element);
const div = element.shadowRoot.querySelector('div');
expect(div.textContent).toBe('Hello, World!');
});
});
VS Code is showing me an error line under createElement in my import statement:
Module '"lwc"' has no exported member 'createElement'.ts(2305).
When I run the test I get a failure with the following message:
TypeError: Cannot read properties of null (reading 'textContent').
All LWC examples I could find use createElement yet there are GitHub PRs from years ago with comments saying that createElement api will soon be deprecated.
The only other way I could find to possibly create an element is from a comment in the base LightningComponent class within the framework itself:
customElements.define('tut-hello', Hello.CustomElementConstructor);
const element = document.createElement('tut-hello', {
is: 'tut-hello'
});
But this doesn't work for me either. How do I create an element for testing with the latest version of LWC OSS?

Related

How to gracefully handle errors in responses on a component level with Axios and Vue 3?

I am sorry if it is obvious/well-covered elsewhere, but my google-fu has been failing me for over a full day by now. What I would like to achieve is a rich component-level handling of request errors: toaster notifications, status bars, you name it. The most obvious use case is auth guards/redirects, but there may be other scenarios as well (e.g. handling 500 status codes). For now, app-wide interceptors would do, but there is an obvious (to me, at least) benefit in being able to supplement or override higher-level interceptors. For example, if I have interceptors for 403 and 500 codes app-wide, I might want to override an interceptor for 403, but leave an interceptor for 500 intact on a component level.
This would require access to component properties: I could then pass status messages in child components, create toaster notifications with custom timeouts/animations and so on. Naively, for my current app-wide problem, this functionality belongs in App.vue, but I can not figure out how to get access to App in axios.interceptors.response using the current plugin arrangement and whether it is okay to use a single axios instance app-wide in the first place.
The trimmed down code I have tried so far (and which seems the most ubiquitous implementation found online) can be found below. It works with redirects, producing Error: getTranslators: detection is already running in the process (maybe because another 401 happens right after redirect with my current testing setup). However, import Vue, both with curly brackets and without, fails miserably, and, more importantly, I have no way of accessing app properties and child components from the plugin.
// ./plugins/axios.js
import axios from 'axios';
import { globalStorage } from '#/store.js';
import router from '../router';
// Uncommenting this import gives Uncaught SyntaxError: ambiguous indirect export: default.
// Circular dependency?..
// import Vue from 'vue';
const api = axios.create({
baseURL: import.meta.env.VUE_APP_API_URL,
});
api.interceptors.response.use(response => response,
error => {
if (error.response.status === 401) {
//Vue.$toast("Your session has expired. You will be redirected shortly");
delete globalStorage.userInfo;
localStorage.setItem('after_login', router.currentRoute.value.name);
localStorage.removeItem('user_info');
router.push({ name: 'login' });
}
return Promise.reject(error);
});
export default api;
// --------------------------------------------------------------------------
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import axios from './plugins/axios'
import VueAxios from 'vue-axios'
const app = createApp(App)
app.use(router)
  .use(VueAxios, axios)
  .mount('#app')
So, then, how do I get access to component properties in interceptors? If I need them to behave differently for different components, would I then need multiple axios instances (assuming the behavior is not achieved by pure composition)? If so, where to put the relevant interceptor configuration and how to ensure some parts of global configuration such as baseURL apply to all of these instances?
I would prefer not having more major external dependencies such as Vuex as a complete replacement for the existing solution, but this is not a hill to die on, of course.
Instead of using axios's interceptors, you should probably create a composable. Consider the following:
composables/useApiRequest.js
import axios from 'axios';
import { useToast } from "vue-toastification";
const useApiRequest = () => {
const toast = useToast();
const fetch = async (url) => {
try {
await axios.get(url);
} catch (error) {
if (error.response.status === 403) {
toast.error("Your session has expired", {
timeout: 2000
});
}
}
};
return {
fetch,
};
};
export default useApiRequest;
Here we're creating a composable called useApiRequest that serves as our layer for the axios package where we can construct our api requests and create generic behaviors for certain response attributes. Take note that we can safely use Vue's Composition API functions and also components such as the vue-toastification directly in this composable:
if (error.response.status === 403) {
toast.error("Your session has expired", {
timeout: 2000
});
}
We can import this composable in the component and use the fetch function to send a GET request to whatever url that we supply:
<script setup>
import { ref } from 'vue';
import useApiRequest from '../composables/useApiRequest';
const searchBar = ref('');
const request = useApiRequest();
const retrieveResult = async () => {
await request.fetch(`https://api.ebird.org/v2/data/obs/${searchBar.value}/recent`);
}
</script>
And that's it! You can check the example here.
Now, you mentioned that you want to access component properties. You can accomplish this by letting your composable accept arguments containing the component properties:
// `props` is our component props
const useApiRequest = (props) => {
// add whatever logic you plan to implement for the props
}
<script setup>
import { ref } from 'vue';
import useApiRequest from '../composables/useApiRequest';
import { DEFAULT_STATUS } from '../constants';
const status = ref(DEFAULT_STATUS);
const request = useApiRequest({ status });
</script>
Just try to experiment and think of ways to make the composable more reusable for other components.
Note
I've updated the answer to change "hook" to "composable" as this is the correct term.

Svelte/SvelteKit: Dynamic import of components with variable

I want to dynamically import components without importing a specific component.
I want to set the component name with a variable, received from the store:
<script lang="ts">
// SVELTE
import { onMount } from 'svelte';
// STORE
import { dynamicComponent } from '$stores/dynamicTitle';
$: $dynamicComponent;
console.log($dynamicComponent)
let renderDynamicComponent
onMount(async () => {
const importValue = (await import(`../../lib/components/Home/DynamicComponents/${String($dynamicComponent)}.svelte`)).default;
// const importValue = (await import(`../../lib/components/Home/DynamicComponents/IntroSectionCustom.svelte`)).default;
renderDynamicComponent = importValue
});
<svelte:component this={renderDynamicComponent}/>
But I get:
Uncaught (in promise) TypeError: Failed to fetch dynamically imported module: http://localhost:3000/src/lib/components/Home/DynamicComponents/Intro-Section-Custom.svelte
I do not understand. From the error, it seems to be the right path ...
The Rollup plugin #rollup/plugin-dynamic-import-vars might be of help here. I haven't used it with SvelteKit specifically, but it worked fine with standard Svelte with Vite as bundler.
// Example.svelte
function importLocale(locale) {
return import(`./locales/${locale}.js`);
}
// vite.config.js
import dynamicImportVars from '#rollup/plugin-dynamic-import-vars';
export default (mode) =>
defineConfig({
plugins: [
dynamicImportVars({
include: './src/Example.svelte'
})
]
});
SvelteKit uses Vite behind the scenes, but has its own configuration format. In svelte.config.js, pass dynamicImportVars() to the config.vite.plugins key:
// svelte.config.js
/** #type {import('#sveltejs/kit').Config} */
const config = {
vite: {
plugins: [
dynamicImportVars({
include: './src/Example.svelte'
})
]
}
};
export default config;
Please take note of the limitations mentioned in the README of the Rollup plugin.
I don't think Svelte + the bundler currently support dynamically generated paths:
let thing = 'Thing';
Thing = (await import(`./${thing}.svelte`)).default; // this won't work
Thing = (await import(`./Thing.svelte`)).default; // this will work
limitation of the bundler.
github issue: https://github.com/sveltejs/svelte/issues/6702
What you're doing does work if not using import vars. When adding an import variable you need make your renderDynamicComponent identifier reactive. So instead of this:
let renderDynamicComponent
Do this:
$: renderDynamicComponent = null
This will allow svelte:component to render your imported component with dynamic path variable. This seems to be a special case when using dynamic import vars with Vite.

Meteor + React: Can't get data from mongoDB Collection

I have a Meteor Application that I'm developing with React. I still have the autopublish package in my project (autopublish#1.0.7).
Here is my relevant code:
MainMenu.jsx
import React, { Component, PropTypes } from 'react'
import { Meteor } from 'meteor/meteor'
import { FlowRouter } from 'meteor/kadira:flow-router'
import { createContainer } from 'meteor/react-meteor-data'
import { ChatRooms } from '/imports/api/chatrooms.js'
export class MainMenu extends Component {
render() {
console.log(this.props.chatrooms)
return (
{/* Render stuff here is not part of the scope of this question */}
)
}
}
MainMenu.PropTypes = {
chatrooms: PropTypes.array.isRequired
}
export default createContainer(() => {
return {
chatrooms: ChatRooms.find({}).fetch()
}
}, MainMenu)
chatrooms.js
import { Mongo } from 'meteor/mongo'
export const ChatRooms = new Mongo.Collection('chatrooms')
The console.log(this.props.chatrooms) in the MainMenu Component always returns an empty array ([]).
There are definitely items in the Mongo Database because when I run the meteor mongo command in my console and type db.chatrooms.find({}); it returns the 3 items that I've inserted to test this all.
Anyone have any idea what I may be doing wrong here? Help would be much appreciated!
I've figured it out. I left out a crucial step of this whole process.
In my /server/main.js file I needed to add the following line which fixed everything:
import '../imports/api/chatrooms.js

undefined is not a function (evaluating '_reactNativeMeteor2.default.collection("messages").find().fetch()')

In my Meteor app I have a collection definition like this:
this.collections.Messages = new Mongo.Collection("messages");
Now I try to connect to it from a react native meteor like this:
import React, { Component } from 'react';
import Meteor, { createContainer } from 'react-native-meteor';
import MessageListComponent from '../routes/messageList';
export default MessageListContainer = createContainer(() => {
const messagesHandle = Meteor.subscribe('userMessage');
const loading = !messagesHandle.ready();
const messages = Meteor.collection("messages").find().fetch();
return {
loading,
messages
};
}, MessageListComponent);
But it's return below red error message on device:
undefined is not a function (evaluating '_reactNativeMeteor2.default.collection("messages").find().fetch()')
What is the problem guys?
Try eliminating the fetch() from your messages const:
const messages = Meteor.collection('messages').find();
The fetch converts the cursor into an array, and probably isn't necessary here. Also, this line is the only one where you have double quotes, but I'm not sure that that is relevant.

How to properly subscribe to collection on Meteor client side?

First of all, I'm not a newbie to Meteor, but after the latest Meteor updates I have to re-study the framework, and now I'm having trouble using Meteor subscription on the client side.
To be specific, I have subscribed a collection on the client side, however when I try to refer to it the browser console reported the error:
Exception in template helper: ReferenceError: Chatbox is not defined
Here's the structure of my code:
imports/api/chatbox/chatboxes.js
// define the collection
export const Chatbox = new Mongo.Collection("chatbox");
imports/api/chatbox/server/publication.js - to be imported in server/main.js
import { Meteor } from "meteor/meteor";
import { Chatbox } from "../chatboxes";
Meteor.publish("chatbox", function(parameter) {
return Chatbox.find(parameter.find, parameter.options);
});
imports/ui/chatbox/chatbox.js - page template to be rendered as content upon routing
import { Template } from 'meteor/templating';
import { ReactiveDict } from 'meteor/reactive-dict';
import './chatbox.html';
import './chatbox.css';
Template.chatbox.onCreated(function bodyOnCreated() {
this.state = new ReactiveDict();
// create subscription query
var parameters = {
find: {
// query selectors
permission: "1001",
},
options: {
// query options
}
};
Meteor.subscribe("chatbox", parameters);
});
Template.chatbox.helpers({
canAddMore() {
// Chatbox collection direct access from client
return Chatbox.find().count() < 3;
},
});
I'd appreciate if you can help me with this issue. Thanks all for taking your time reading my question!
Regards
You need to import Chatbox in imports/ui/chatbox/chatbox.js:
import { Template } from 'meteor/templating';
import { ReactiveDict } from 'meteor/reactive-dict';
import { Chatbox } from "../chatboxes"; // correct this path
It's undefined right now because it hasn't been imported.