I am trying to test my SEO component which looks like this:
export const Seo: React.FC<Props> = ({ seo, title, excerpt, heroImage }) => {
const description = seo?.description || excerpt
const pageTitle = seo?.title || title
const router = useRouter()
return (
<NextSeo // https://www.npmjs.com/package/next-seo
canonical={seo?.canonical}
nofollow={seo?.nofollow}
noindex={seo?.noindex}
title={pageTitle}
description={description}
openGraph={{
title,
description,
type: "article",
...
and my test is like so:
describe("Seo", () => {
it("should render the meta tags", async () => {
const props = {
title: "title page",
excerpt: "string",
seo: {
title: "seo title",
description: "meta description",
},
heroImage: {
src: "url",
alt: "alt text",
width: 300,
height: 400,
},
}
function getMeta(metaName: string) {
const metas = document.getElementsByTagName("meta")
for (let i = 0; i < metas.length; i += 1) {
if (metas[i].getAttribute("name") === metaName) {
return metas[i].getAttribute("content")
}
}
return ""
}
render(<Seo {...props} />)
await waitFor(() => expect(getMeta("title")).toEqual("title page"))
})
})
however the test is failing: (it looks like the head element is empty)
I was having the same issue, but I found this answer on GitHub.
So basically you need to mock next/head, pass document.head to the container property of render's options and finally access the document.
Your test would end up like this:
jest.mock('next/head', () => {
return {
__esModule: true,
default: ({ children }: { children: Array<React.ReactElement> }) => {
return <>{children}</>;
},
};
});
describe("Seo", () => {
it("should render the meta tags", () => {
const props = {
title: "title page",
excerpt: "string",
seo: {
title: "seo title",
description: "meta description",
},
heroImage: {
src: "url",
alt: "alt text",
width: 300,
height: 400,
},
}
render(<Seo {...props} />, { container: document.head })
expect(document.title).toBe("title page")
})
})
In my case I didn't test it with that getMeta function but I believe it would work too.
in JS
jest.mock('next/head', () => {
return {
__esModule: true,
default: ({ children }) => {
return children;
}
};
});
Related
I have been trying to create a simple auto complete using Quasar's select but I'm not sure if this is a bug or if I'm doing something wrong.
Problem
Whenever I click the QSelect component, it doesn't show the dropdown where I can pick the options from.
video of the problem
As soon as I click on the QSelect component, I make a request to fetch a list of 50 tags, then I populate the tags to my QSelect but the dropdown doesn't show.
Code
import type { PropType } from "vue";
import { defineComponent, h, ref } from "vue";
import type { TagCodec } from "#/services/api/resources/tags/codec";
import { list } from "#/services/api/resources/tags/actions";
import { QSelect } from "quasar";
export const TagAutoComplete = defineComponent({
name: "TagAutoComplete",
props: {
modelValue: { type: Array as PropType<TagCodec[]> },
},
emits: ["update:modelValue"],
setup(props, context) {
const loading = ref(false);
const tags = ref<TagCodec[]>([]);
// eslint-disable-next-line #typescript-eslint/ban-types
const onFilterTest = (val: string, doneFn: (update: Function) => void) => {
const parameters = val === "" ? {} : { title: val };
doneFn(async () => {
loading.value = true;
const response = await list(parameters);
if (val) {
const needle = val.toLowerCase();
tags.value = response.data.data.filter(
(tag) => tag.title.toLowerCase().indexOf(needle) > -1
);
} else {
tags.value = response.data.data;
}
loading.value = false;
});
};
const onInput = (values: TagCodec[]) => {
context.emit("update:modelValue", values);
};
return function render() {
return h(QSelect, {
modelValue: props.modelValue,
multiple: true,
options: tags.value,
dense: true,
optionLabel: "title",
optionValue: "id",
outlined: true,
useInput: true,
useChips: true,
placeholder: "Start typing to search",
onFilter: onFilterTest,
"onUpdate:modelValue": onInput,
loading: loading.value,
});
};
},
});
What I have tried
I have tried to use the several props that is available for the component but nothing seemed to work.
My understanding is that whenever we want to create an AJAX request using QSelect we should use the onFilter event emitted by QSelect and handle the case from there.
Questions
Is this the way to create a Quasar AJAX Autocomplete? (I have tried to search online but all the answers are in Quasar's forums that are currently returning BAD GATEWAY)
What am I doing wrong that it is not displaying the dropdown as soon as I click on the QSelect?
It seems updateFn may not allow being async. Shift the async action a level up to solve the issue.
const onFilterTest = async (val, update /* abort */) => {
const parameters = val === '' ? {} : { title: val };
loading.value = true;
const response = await list(parameters);
let list = response.data.data;
if (val) {
const needle = val.toLowerCase();
list = response.data.data.filter((x) => x.title.toLowerCase()
.includes(needle));
}
update(() => {
tags.value = list;
loading.value = false;
});
};
I tested it by the following code and mocked values.
// import type { PropType } from 'vue';
import { defineComponent, h, ref } from 'vue';
// import type { TagCodec } from "#/services/api/resources/tags/codec";
// import { list } from "#/services/api/resources/tags/actions";
import { QSelect } from 'quasar';
export const TagAutoComplete = defineComponent({
name: 'TagAutoComplete',
props: {
modelValue: { type: [] },
},
emits: ['update:modelValue'],
setup(props, context) {
const loading = ref(false);
const tags = ref([]);
const onFilterTest = async (val, update /* abort */) => {
// const parameters = val === '' ? {} : { title: val };
loading.value = true;
const response = await new Promise((resolve) => {
setTimeout(() => {
resolve({
data: {
data: [
{
id: 1,
title: 'Vue',
},
{
id: 2,
title: 'Vuex',
},
{
id: 3,
title: 'Nuxt',
},
{
id: 4,
title: 'SSR',
},
],
},
});
}, 3000);
});
let list = response.data.data;
if (val) {
const needle = val.toLowerCase();
list = response.data.data.filter((x) => x.title.toLowerCase()
.includes(needle));
}
update(() => {
tags.value = list;
loading.value = false;
});
};
const onInput = (values) => {
context.emit('update:modelValue', values);
};
return function render() {
return h(QSelect, {
modelValue: props.modelValue,
multiple: true,
options: tags.value,
dense: true,
optionLabel: 'title',
optionValue: 'id',
outlined: true,
useInput: true,
useChips: true,
placeholder: 'Start typing to search',
onFilter: onFilterTest,
'onUpdate:modelValue': onInput,
loading: loading.value,
});
};
},
});
I've tried many different ways to do this with both alert() and simple if-else statements, and even what is at this link: https://v2.vuejs.org/v2/cookbook/form-validation.html
But nothing works! What am I doing wrong?
What is in my index.html:
<div id="app">
<div class = "addTask">
<h1>A List of Tasks</h1>
<p v-show="activeItems.length === 0">You are done with all your tasks! Celebrate!</p>
<form #submit.prevent="addItem">
<input type="text" v-model="title">
<button type="submit">+</button>
</form>
</div>
This is what I have in scripts.js:
var app = new Vue({
el: '#app',
data () {
return {
// errors: [],
items: [{
userId: 0,
id: 0,
title: "",
completed: false,
}],
title: '',
show: 'all',
}
},
// Using axios to asynchrously query the API
mounted () {
axios
.get("https://jsonplaceholder.typicode.com/todos")
.then(response => this.items = response.data)
},
computed: {
activeItems() {
this.saveItems;
return this.items.filter(item => {
return !item.completed;
});
},
filteredItems() {
// An active state denotes the task is not yet completed
if (this.show === 'active')
return this.items.filter(item => {
return !item.completed;
});
if (this.show === 'completed')
return this.items.filter(item => {
return item.completed;
});
// This makes it so added tasks go to the top, not bottom
return this.items.reverse();
},
},
methods: {
addItem() {
if(this.items != 0) {
this.items.push({
title: this.title,
completed: false
})
this.title = "";
}
else {
alert("Please enter a task.")
}
Based on your code, you should declare a property called title in the data function and check if title is empty or not in addItem method.
var app = new Vue({
el: '#app',
data () {
return {
// errors: [],
title: '', // add this property in data function
items: [{
userId: 0,
id: 0,
title: "",
completed: false,
}],
title: '',
show: 'all',
}
},
// Using axios to asynchrously query the API
mounted () {
axios
.get("https://jsonplaceholder.typicode.com/todos")
.then(response => this.items = response.data)
},
computed: {
activeItems() {
this.saveItems;
return this.items.filter(item => {
return !item.completed;
});
},
filteredItems() {
// An active state denotes the task is not yet completed
if (this.show === 'active')
return this.items.filter(item => {
return !item.completed;
});
if (this.show === 'completed')
return this.items.filter(item => {
return item.completed;
});
// This makes it so added tasks go to the top, not bottom
return this.items.reverse();
},
},
methods: {
addItem() {
if(this.title !== '') { //check if `title` is empty or not
this.items.push({
title: this.title,
completed: false
})
this.title = "";
}
else {
alert("Please enter a task.")
}
I created an example in Stackblitz. You can also view the example directly by clicking here
I'm writing a small CLI in typescript and I have a command which basically allows me to generate a json file with default values in it (just like npm init -y), but I don't know how to auto answer the questions in inquirer.
This is what I've got so far:
export const initializeConfig = (project: string, ...args: boolean[]) => {
prompt([
{
type: "input",
name: "name",
message: "What is the name of the project?",
default: basename(cwd()),
when: () => args.every((arg) => arg === false),
},
{
type: "list",
name: "project",
message: "What is the type of the project?",
choices: ["Node", "Python"],
default: project,
when: () => args.every((arg) => arg === false),
},
])
.then((answers: Answers) => {
config = setConfig({ name: answers.name });
config = setConfig({ project: answers.project });
})
.then(() =>
prompt([
{
type: "input",
name: "path",
message: "Where is your project root located?",
default: ".",
when: () => args.every((arg) => arg === false),
},
{
type: "input",
name: "ignore",
message: "What do you want to ignore? (comma separated)",
default: defaultIgnores(config.project).ignore,
when: () => args.every((arg) => arg === false),
},
]).then((answers: Answers) => {
config = setConfig(ignoreFiles(config.project, answers.ignore));
createConfig(answers.path, config);
})
);
};
I thought that if I'd skip/hide the questions with when(), it would use the default values, but it doesn't. It's always undefined.
Didn't find this topic on the internet so far. Any ideas?
Kind of a life hack, but I managed to "auto answer" my questions in inquirer by creating a defaults() function that returns an object of the default values.
Then I can use those if my answer object is empty as you see below:
const defaults = (project: string) => {
return {
name: basename(cwd()),
project,
path: ".",
ignore: defaultIgnores(project).ignore,
};
};
export let config: any = {
version,
};
export const initializeConfig = (project: string, ...args: boolean[]) => {
prompt([
{
type: "input",
name: "name",
message: "What is the name of the project?",
default: defaults(project).name,
when: () => args.every((arg) => arg === false),
},
{
type: "list",
name: "project",
message: "What is the type of the project?",
choices: ["Node", "Python"],
default: defaults(project).project,
when: () => args.every((arg) => arg === false),
},
])
.then((answers: Answers) => {
const { name, project: projectName } = defaults(project);
config = setConfig({ name: answers.name || name });
config = setConfig({ project: answers.project || projectName });
})
.then(() =>
prompt([
{
type: "input",
name: "path",
message: "Where is your project root located?",
default: defaults(project).path,
when: () => args.every((arg) => arg === false),
},
{
type: "input",
name: "ignore",
message: "What do you want to ignore? (comma separated)",
default: defaults(project).ignore,
when: () => args.every((arg) => arg === false),
},
]).then((answers: Answers) => {
const { ignore, path } = defaults(project);
config = setConfig(
ignoreFiles(config.project, (answers.ignore || ignore)!)
);
createConfig(answers.path || path, config);
})
);
};
I have encountered a problem while making a personal Gatsby site whereby blog post files are having thier unique folders included in the slug.
For example a file structure of:
data
|
|– blog
|
|– blog-1
| |
| |-blog-1.mdx
| |-img.jpg
|
|– blog-2
| |
| |-blog-2.mdx
| |-img.jpg
|
|– blog-3
| |
| |-blog-3.mdx
| |-img.jpg
Will, for example, produce slugs like this
{
"data": {
"allMdx": {
"edges": [
{
"node": {
"fields": {
"slug": "/blog-1/blog-1/"
},
"frontmatter": {
"title": "Blog 1",
"posttype": "blog"
}
}
},
{
"node": {
"fields": {
"slug": "/blog-2/blog-2/"
},
"frontmatter": {
"title": "Blog 2",
"posttype": "blog"
}
}
},
{
"node": {
"fields": {
"slug": "/blog-3/blog-3/"
},
"frontmatter": {
"title": "Blog 3",
"posttype": "blog"
}
}
},
I expect them to produce a slug like this:
{
"node": {
"fields": {
"slug": "/blog-1/"
},
"frontmatter": {
"title": "Blog 1",
"posttype": "blog"
}
}
},
The path to the parent blog folder is included in my gatsby-config like this:
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/data/blog`,
name: `blog`,
},
},
And then my gatsby-node folder is set up like this:
const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)
exports.createPages = ({ graphql, actions }) => {
const { createPage } = actions
const blogPost = path.resolve(`./src/templates/blog-post.js`)
const portfolioPost = path.resolve(`./src/templates/portfolio-post.js`)
const journeyPost = path.resolve(`./src/templates/journey-post.js`)
return graphql(
`
{
allMdx(
sort: { fields: [frontmatter___date], order: DESC }
limit: 1000
) {
edges {
node {
fields {
slug
}
frontmatter {
title
posttype
}
}
}
}
}
`
).then(result => {
if (result.errors) {
throw result.errors
}
const posts = result.data.allMdx.edges
posts.forEach((post, index) => {
const previous = index === posts.length - 1 ? null : posts[index + 1].node
const next = index === 0 ? null : posts[index - 1].node
if (post.node.frontmatter.posttype == "portfolio") {
createPage({
path: `/portfolio${post.node.fields.slug}`,
component: portfolioPost,
context: {
slug: post.node.fields.slug,
previous,
next,
},
})
} else if (post.node.frontmatter.posttype == "journey") {
createPage({
path: `/journey${post.node.fields.slug}`,
component: journeyPost,
context: {
slug: post.node.fields.slug,
previous,
next,
},
})
} else {
createPage({
path: `/blog${post.node.fields.slug}`,
component: blogPost,
context: {
slug: post.node.fields.slug,
previous,
next,
},
})
}
})
return null
})
}
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
if (node.internal.type === `Mdx`) {
const value = createFilePath({ node, getNode })
createNodeField({
name: `slug`,
node,
value,
})
}
}
Note that the journey and portfolio paths in this file are at this point in time doing exactly the same thing as the blog path. They are set up in exactly the same way and are just split out depending on posttype. Pages are created fine but they are all using the unwanted folder/file.mdx slug.
Fixed by looking at other blog examples.
Post filename needs to be index.md or index.mdx
I am sure there is a better way but I was able to solve it by making changes in gatsby-node.js to only take the substring after the last slash (/) in from the file path. If someone knows a better way I will be glad to know that.
Old:
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
if (node.internal.type === `MarkdownRemark`) {
const value = createFilePath({ node, getNode })
createNodeField({
name: `slug`,
node,
value,
})
}
}
New:
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
if (node.internal.type === `MarkdownRemark`) {
const value = createFilePath({ node, getNode, trailingSlash:false })
createNodeField({
name: `slug`,
node,
value: `${value.indexOf("/") > -1 ? value.substring(value.lastIndexOf("/")) : value}`,
})
}
}
Being a newbie to the React community...I'm blocked (for hours now) and unable to trace a solution to fix the error posted above:
Am I missing the right parameters to how the data object is fetched in through the app?
This is my ajax data response
The bug is living on props.personList.map inside of const ListContainer
For context here's the code on the entire file:
import React from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';
function getPersonList() {
const api = 'apistring';
return axios.get(api).then(res => {
console.log(res);
}).catch(err => console.log(err));
}
let getLastName = (fullName) => {
return fullName.match(/\w+/g)[1];
};
const getFirstName = (fullName) => {
return fullName.match(/\w+/g)[0];
};
//Remove any people that do not have the name we are searching for
let filterByName = (searchForName, personList) => {
return personList.filter((person) => {
return person.name === searchForName;
});
};
//VIEW (React)
const Search = ({ onChange }) => React.DOM.input({
type: 'input',
onChange
});
const Thumbnail = ({src}) => React.DOM.img({
className: 'image',
src
});
//CODE BREAKS HERE
const ListRow = (props) => React.DOM.tr({ key: props.person.name }, [
React.DOM.td({ key: 'headshot' }, React.createElement(Thumbnail, { src: props.person.url })),
React.DOM.td({ key: 'firstName' }, null, getFirstName(props.person.name)),
React.DOM.td({ key: 'lastName' }, null, getLastName(props.person.name)),
]);
const ListContainer = (props) => React.DOM.table({ className: 'list-container' }, [
React.DOM.thead({ key: 'firstName' }, React.DOM.tr({}, [
React.DOM.th({ key: 'lastName' }, null, 'headshot'),
React.DOM.th({ key: 'id' }, null, 'First Name'),
React.DOM.th({ key: 'last-h' }, null, 'Last Name')
])),
React.DOM.tbody({ key: 'tbody' }, props.personList.map((person, i) =>
React.createElement(ListRow, { key: `person-${i}`, person })))
]);
const App = React.createClass({
getInitialState() {
return {
personList: [],
visiblePersonList: []
};
},
componentDidMount() {
getPersonList().then((data) =>
this.setState({
data,
visiblePersonList: data
}));
},
_shuffleList() {
this.setState({
visiblePersonList: shuffleList(this.state.personList)
});
},
_sortByFirst() {
this.setState({
visiblePersonList: sortByFirstName(this.state.personList)
});
},
_sortByLast() {
this.setState({
visiblePersonList: sortByLastName(this.state.personList)
});
},
_onSearch(e) {
this.setState({
visiblePersonList: filterByName(e.target.value, this.state.personList)
});
},
render() {
const { visiblePersonList } = this.state;
return React.DOM.div({ className: 'app-container' }, [
React.createElement(Search, { key: 'search', onChange: this._onSearch }),
React.DOM.button({ key: 'shuffle', onClick: this._shuffleList }, null, 'Shuffle'),
React.DOM.button({ key: 'sort-first', onClick: this._sortByFirst }, null, 'Sort (First Name)'),
React.DOM.button({ key: 'sort-last', onClick: this._sortByLast }, null, 'Sort (Last Name)'),
React.createElement(ListContainer, { key: 'list', personList: visiblePersonList })
]);
}
});
ReactDOM.render(<App />, document.getElementById('app'));
Your callback with the console.log accesses the value and then discards it.
function getPersonList() {
const api = 'apistring';
return axios.get(api).then(res => {
console.log(res);
}).catch(err => console.log(err));
}
should be
function getPersonList() {
const api = 'apistring';
return axios.get(api).then(res => {
console.log(res);
return res.data.items;
}).catch(err => console.log(err));
}
When you use .then you are inserting a link into your promise chain. The value returned by .then becomes the value passed to the next handler. Because you are not returning any value, your
getPersonList().then((data) => {
// ...
});
callback will get data as undefined.
Another thing to note, though don't cause this specific error, is that your screenshot shows objects with .firstName and .lastName but all of the code in this file uses .name which does not exist.