Apollo=Express GraphQL error, cannot find module graphql - express

I am trying to set up a graphql express server with apollo, express, typeorm, and graphql, however I am getting this error now that I have implemented the UserResolver.ts file. I have already tried installing graphql and even have downgraded it to 14.2.1 but it still doesn't work. Here is the source code:
index.ts
import "reflect-metadata";
import express from 'express';
import {ApolloServer} from 'apollo-server-express';
import { buildSchema } from "type-graphql";
import { UserResolver } from "./UserResolver";
(async () => {
const app = express();
app.get('/', (_req, res) => res.send('hello'))
const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [UserResolver]
})
});
// my line idk if it works
await apolloServer.start();
apolloServer.applyMiddleware({ app });
app.listen(4000, () => {
console.log("express server started");
});
})()
// createConnection().then(async connection => {
// console.log("Inserting a new user into the database...");
// const user = new User();
// user.firstName = "Timber";
// user.lastName = "Saw";
// user.age = 25;
// await connection.manager.save(user);
// console.log("Saved a new user with id: " + user.id);
// console.log("Loading users from the database...");
// const users = await connection.manager.find(User);
// console.log("Loaded users: ", users);
// console.log("Here you can setup and run express/koa/any other framework.");
// }).catch(error => console.log(error));
UserResolver.ts
import { Query, Resolver } from 'type-graphql';
#Resolver()
export class UserResolver{
#Query(() => String)
hello(){
return "hi";
}
}
ormconfig.json
{
"type": "postgres",
"host": "localhost",
"port": 5432,
"username": "postgres",
"password": "7434006a",
"database": "graphqldatabase",
"synchronize": true,
"logging": false,
"entities": [
"src/entity/**/*.ts"
],
"migrations": [
"src/migration/**/*.ts"
],
"subscribers": [
"src/subscriber/**/*.ts"
],
"cli": {
"entitiesDir": "src/entity",
"migrationsDir": "src/migration",
"subscribersDir": "src/subscriber"
}
}
package.json
{
"name": "backend",
"version": "0.0.1",
"description": "Awesome project developed with TypeORM.",
"devDependencies": {
"#types/express": "^4.17.13",
"#types/graphql": "^14.5.0",
"#types/node": "^8.0.29",
"ts-node": "3.3.0",
"typescript": "3.3.3333"
},
"dependencies": {
"#nestjs/common": "^8.2.3",
"#nestjs/core": "^8.2.3",
"#nestjs/typeorm": "^8.0.2",
"apollo-server-express": "^3.5.0",
"express": "^4.17.1",
"graphql": "^14.2.1",
"pg": "^8.4.0",
"reflect-metadata": "^0.1.10",
"rxjs": "^7.4.0",
"typeorm": "0.2.41"
},
"scripts": {
"start": "ts-node src/index.ts",
"typeorm": "node --require ts-node/register ./node_modules/typeorm/cli.js --config src/config/postgres.config.ts"
}
}

Related

NextJS - NextAuth: getToken always return null in middleware.ts

I can't get the token data using getToken({req, secret}), always return null.
Next JS 13.1.1 and next-auth 4.3.4
package.json:
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"deploy": "npx serverless"
},
"dependencies": {
"#emotion/react": "^11.10.0",
"#emotion/styled": "^11.10.0",
"#mui/icons-material": "^5.8.4",
"#mui/lab": "^5.0.0-alpha.95",
"#mui/material": "^5.10.0",
"#reduxjs/toolkit": "^1.8.4",
"animate.css": "^4.1.1",
"chart.js": "^3.9.1",
"cookie": "^0.5.0",
"html-to-image": "^1.11.4",
"jsonwebtoken": "^9.0.0",
"jspdf": "^2.1.0",
"moment": "^2.29.4",
"next": "^13.1.1",
"next-auth": "^4.3.4",
"react": "^18.2.0",
"react-chartjs-2": "^4.3.1",
"react-cookie": "^4.1.1",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.2",
"react-hook-form": "^7.34.1",
"react-redux": "^8.0.2",
"sass": "^1.54.4",
"swr": "^1.3.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"#types/cookie": "^0.5.1",
"#types/jsonwebtoken": "^9.0.0",
"#types/node": "18.7.5",
"#types/react": "18.0.17",
"#types/react-dom": "18.0.6",
"#types/uuid": "^8.3.4",
"eslint": "8.22.0",
"eslint-config-next": "^13.1.1",
"typescript": "4.7.4"
}
}
[...nextauth].js:
import NextAuth, { NextAuthOptions } from "next-auth";
// Providers
import Credentials from "next-auth/providers/credentials";
// Services
import { authService, refreshAccessToken } from "../../../services";
export const authOptions: NextAuthOptions = {
providers: [
Credentials({
name: "Credentials",
credentials: {
username: {
label: "Username",
type: "text",
placeholder: "C.C.",
autoComplete: true,
},
password: {
label: "Password",
type: "password",
placeholder: "Type your password...",
autoComplete: true,
},
},
async authorize(credentials) {
return await authService({
username: credentials!.username,
password: credentials!.password,
});
},
}),
],
pages: {
signIn: "/auth/login",
},
session: {
strategy: "jwt",
maxAge: 60 * 60 * 7, // 7 hours
updateAge: 60 * 60 * 2,
},
callbacks: {
async jwt({ token, account, user }) {
if (user) {
token.accessToken = user.accessToken;
token.refreshToken = user.refreshToken;
if (account) {
switch (account.type) {
case "credentials":
token.user = user.user;
token.accessTokenExpires = user.accessTokenExpires;
break;
}
}
}
if (Date.now() / 1000 < (token.accessTokenExpires as number)) {
return token;
}
return await refreshAccessToken(token);
},
async session({ session, token }) {
session.accessToken = token.accessToken;
session.user = token.user as any;
return session;
},
},
// secret: process.env.NEXTAUTH_SECRET!,
secret: process.env.NEXTAUTH_SECRET,
};
export default NextAuth(authOptions);
and middleware.ts:
import { getToken } from "next-auth/jwt";
import { withAuth } from "next-auth/middleware";
import { NextResponse } from "next/server";
// Interfaces
import { IUser } from "./interfaces";
const secret = process.env.NEXTAUTH_SECRET;
export default withAuth(async function middleware(req) {
const token = await getToken({ req, secret }); // ALWAYS returns null
const { user } = (token as { user: IUser; accessToken: string }) || {};
const isAuth = !!token;
const isAuthPage = req.nextUrl.pathname.startsWith("/auth");
if (isAuthPage) {
if (isAuth) {
// return NextResponse.redirect(new URL(`/${user.hierarchy}`, req.url));
return NextResponse.redirect(new URL(`/role`, req.url));
}
return null;
}
if (!isAuth) {
let from = req.nextUrl.pathname;
if (req.nextUrl.search) {
from += req.nextUrl.search;
}
return NextResponse.redirect(new URL(`/auth/login`, req.url));
}
});
process.env.NEXTAUTH_SECRET = secret.
I tried to debug with nextAuth, but the middleware works fine when the isAuth is false (if i try to load protected route, middleware redirect to /auth), just fail when the getToken function try to get the cookie
to verify auth, what is wrong? :(

Mobx statechange not detected

I have a small simple setup. With mobx and preact.
class AppStore {
loadingobjects = true;
constructor() {
makeObservable(this, {
loadingobjects: observable,
});
this.fetchCommonObjects();
}
fetchCommonObjects = () => {
window
.fetch(url)
.then((res) => res.json())
.then((json) => {
/* data processing */
this.loadingobjects = false;
});
};
}
export const AppStoreContext = createContext();
function AppStoreProvider({ children }) {
return (
<AppStoreContext.Provider value={new AppStore()}>
{children}
</AppStoreContext.Provider>
);
}
export default AppStoreProvider;
export default function useAppStore() {
return useContext(AppStoreContext);
}
const List = observer(() => {
const store = useAppStore();
if (store.loadingobjects) {
return <div class="ui active centered inline loader"></div>;
} else {
return (page content);
}
});
problem is that store.loadingobjects Is always false. Seems like im doing something wrong but i cant put my finger on it...
What am i missing or doing wrong?
Edit addding my configs:
package.json
{
"name": "project",
"version": "0.0.2",
"license": "MIT",
"scripts": {
"start": "set NODE_ENV=dev && webpack serve --mode=development",
"build": "set NODE_ENV=production && webpack -p",
},
"devDependencies": {
"#babel/core": "^7.20.12",
"#babel/plugin-transform-runtime": "^7.19.6",
"#babel/preset-env": "^7.20.2",
"#babel/preset-react": "^7.18.6",
"babel-loader": "^9.1.2",
"babel-plugin-import": "^1.13.6",
"html-webpack-plugin": "^5.5.0",
"surge": "^0.19.0",
"webpack": "^5.75.0",
"webpack-cli": "^5.0.1",
"webpack-dev-server": "^4.11.1"
},
"dependencies": {
"#babel/polyfill": "^7.12.1",
"mobx": "^6.7.0",
"mobx-react": "^7.6.0",
"preact": "^10.11.3"
}
}
webpack.config.js
const path = require('path');
const isProd = (process.env.NODE_ENV === 'production');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
//input
entry: ["#babel/polyfill",'./src'],
resolve: {
alias:{
"react": "preact/compat",
"react-dom": "preact/compat",
"react/jsx-runtime": "preact/jsx-runtime"
}
},
//output
output: {
path : path.join(__dirname, 'build'),
filename : 'bundle.js'
},
//transformations
module: {
rules : [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
}
]
},
//sourcemaps
devtool: 'source-map',
plugins: [new HtmlWebpackPlugin({
template: './src/index.html',
favicon: "./src/favicon.ico"
})],
//server
devServer: {
compress: true,
historyApiFallback: true
}
}
.babelrc
{
"presets": ["#babel/preset-react", ["#babel/preset-env", {"useBuiltIns": "usage",}]],
"plugins": [
["#babel/plugin-transform-runtime"],
[
"#babel/plugin-transform-react-jsx",
{
"pragma": "h",
"pragmaFrag": "Fragment"
}
]
]
}
I found the issue. I started cutting the components into smaller pieces and then adding and removing them into the component hierarchy until i found the component causing the issue. It turned out that i had done onClick={method()} and this was changeing state causing the endless rerenders.

Error when fetching on OpenAI API, error 429

I'm trying to connect OpenAI API to my Vue.js project. Everything is OK but every time I try to POST request, I get a 429 status code (too many request) but I didn't even had the chance to make one. Any help?
Response:
{
"message": "Request failed with status code 429",
"name": "Error",
"stack": "Error: Request failed with status code 429\n at createError (C:\\Users\\sim\\Documents\\SC\\server\\node_modules\\axios\\lib\\core\\createError.js:16:15)\n at settle (C:\\Users\\sim\\Documents\\SC\\server\\node_modules\\axios\\lib\\core\\settle.js:17:12)\n at IncomingMessage.handleStreamEnd (C:\\Users\\sim\\Documents\\SC\\server\\node_modules\\axios\\lib\\adapters\\http.js:322:11)\n at IncomingMessage.emit (events.js:412:35)\n at endReadableNT (internal/streams/readable.js:1333:12)\n at processTicksAndRejections (internal/process/task_queues.js:82:21)",
"config": {
"transitional": {
"silentJSONParsing": true,
"forcedJSONParsing": true,
"clarifyTimeoutError": false
},
"transformRequest": [
null
],
"transformResponse": [
null
],
"timeout": 0,
"xsrfCookieName": "XSRF-TOKEN",
"xsrfHeaderName": "X-XSRF-TOKEN",
"maxContentLength": -1,
"maxBodyLength": -1,
"headers": {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"User-Agent": "OpenAI/NodeJS/3.1.0",
"Authorization": "Bearer secret",
"Content-Length": 137
},
"method": "post",
"data": "{\"model\":\"text-davinci-003\",\"prompt\":\"option-2\",\"temperature\":0,\"max_tokens\":3000,\"top_p\":1,\"frequency_penalty\":0.5,\"presence_penalty\":0}",
"url": "https://api.openai.com/v1/completions"
},
"status": 429
}
My method in Vue.js:
async handleSelect() {
try {
const res = await fetch("http://localhost:8000/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
question: this.selectedOption,
})
})
const data = await res.json();
console.log(data);
} catch {
console.log(data);
}
}
on server side
app.post("/", async (req, res) => {
try {
const question = req.body.question;
const response = await openai.createCompletion({
model: "text-davinci-003",
prompt: `${question}`,
temperature: 0, // Higher values means the model will take more risks.
max_tokens: 3000, // The maximum number of tokens to generate in the completion. Most models have a context length of 2048 tokens (except for the newest models, which support 4096).
top_p: 1, // alternative to sampling with temperature, called nucleus sampling
frequency_penalty: 0.5, // Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.
presence_penalty: 0, // Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.
});
// console.log(response);
res.status(200).send({
bot: response.data.choices[0].text,
});
} catch (error) {
// console.error(error);
res.status(500).send(error || "Something went wrong");
}
});
It's hard to say why your attempt was unsuccessful because you haven't added the full code. The code below works.
Frontend
HelloWorld.vue
<template>
<div class="hello"></div>
<select v-model="selected" #change="handleSelect()">
<option disabled value="">Please select one</option>
<option>Say this is a test</option>
<option>Say nothing</option>
</select>
<div class="container-selected">Selected: {{ selected }}</div>
<div class="container-data" v-if="showData">{{ showData.bot }}</div>
</template>
<script>
export default {
data: function () {
return {
selected: "",
showData: "",
};
},
methods: {
async handleSelect(data) {
try {
const res = await fetch("http://localhost:3000/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
question: this.selected,
}),
});
const data = await res.json();
this.showData = data;
console.log(data);
} catch {
console.log(data);
}
},
},
};
</script>
<style lang="scss">
.container-selected {
margin-top: 12px;
font-size: 20px;
}
.container-data {
margin-top: 24px;
font-size: 20px;
}
</style>
package.json
{
"name": "openai",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
"dependencies": {
"register-service-worker": "^1.7.2",
"vue": "^3.2.13",
"vue-class-component": "^8.0.0-0",
"vue-router": "^4.0.3",
"vuex": "^4.0.0"
},
"devDependencies": {
"#typescript-eslint/eslint-plugin": "^5.4.0",
"#typescript-eslint/parser": "^5.4.0",
"#vue/cli-plugin-eslint": "~5.0.0",
"#vue/cli-plugin-pwa": "~5.0.0",
"#vue/cli-plugin-router": "~5.0.0",
"#vue/cli-plugin-typescript": "~5.0.0",
"#vue/cli-plugin-vuex": "~5.0.0",
"#vue/cli-service": "~5.0.0",
"#vue/eslint-config-typescript": "^9.1.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.0.3",
"prettier": "^2.4.1",
"sass": "^1.32.7",
"sass-loader": "^12.0.0",
"typescript": "~4.5.5"
}
}
Backend
index.js
const express = require('express');
const app = express();
app.use(express.json());
const cors = require('cors');
app.use(cors());
app.post('/', async(req, res) => {
try {
const { Configuration, OpenAIApi } = require('openai');
const configuration = new Configuration({
apiKey: 'sk-xxxxxxxxxxxxxxxxxxxx'
});
const openai = new OpenAIApi(configuration);
const question = req.body.question;
await openai.createCompletion({
model: 'text-davinci-003',
prompt: question,
temperature: 0,
max_tokens: 7
})
.then((response) => {
console.log(response.data.choices[0].text);
res.status(200).send({ bot: response.data.choices[0].text });
})
.catch((err) => {
res.status(400).send({ message: err.message });
})
} catch (error) {
res.status(500).send(error || 'Something went wrong');
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}.`);
});
package.json
{
"name": "openai-server",
"version": "1.0.0",
"description": "Express server",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"nodemon": "^2.0.20",
"openai": "^3.1.0"
}
}
Output
This is the reason you are getting that error, your subscription or free plan has expired
enter image description here

Uncaught (in promise) SyntaxError: Unexpected token < in JSON at position 1 Error with React, Babel and Webpack

I am building a CRUD web application using Webpack, Express, Epilogue, Sequelize, Sqlite3 and React. After setting up Webpack and Babel, I can add entries to the database but not retrieve entries. When I add an entry, if I look at http://localhost:3000/blogposts (one of my endpoints) the page is blank, but in the server output I see my entries get added.
server output in the console
To retrieve entries from the database I use:
getPosts = async () => {
const response = await fetch('/blogposts');
const data = await response.json();
data.forEach(item => item.editMode = false);
this.setState({ data })
}
And I get the error: Uncaught (in promise) SyntaxError: Unexpected token < in JSON at position 1
full error message
I assume this is an error with babel/webpack and await/async.
my webpack.config.js:
const path = require("path")
const HtmlWebPackPlugin = require("html-webpack-plugin")
module.exports = {
entry: {
main: './src/index.js'
},
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/',
filename: '[name].js'
},
target: 'web',
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
},
{
test: /\.html$/,
use: [
{
loader: "html-loader",
options: { minimize: true }
},
]
},
{
test: /\.css$/,
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.(png|svg|jpg|gif)$/,
use: ['file-loader']
},
{
test: /\.json$/,
loader: 'json-loader'
}
]
},
plugins: [
new HtmlWebPackPlugin({
template: "./src/index.html",
filename: "./src/index.html",
excludeChunks: [ 'server' ]
})
],
resolve: {
extensions: ["*",".js", ".jsx", "json"],
alias: {
'components': path.resolve(__dirname, '/components')
}
},
}
my webpackserver.config.js:
const path = require('path')
const nodeExternals = require('webpack-node-externals')
module.exports = {
entry: {
server: './src/server/server.js',
},
output: {
path: path.join(__dirname, 'dist'),
publicPath: '/',
filename: '[name].js'
},
target: 'node',
resolve: {
extensions: ['.js', '.jsx'],
alias: {
'components': path.resolve(__dirname, '/components')
}
},
node: {
__dirname: false,
__filename: false,
},
externals: [nodeExternals()],
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
}
]
}
}
my server.js:
import path from 'path'
import express from 'express'
require('dotenv').config();
const cors = require('cors');
const bodyParser = require('body-parser');
const session = require('express-session');
const { ExpressOIDC } = require('#okta/oidc-middleware');
const Sequelize = require('sequelize');
const epilogue = require('epilogue'), ForbiddenError = epilogue.Errors.ForbiddenError;
const port = process.env.PORT || 3000
const app = express(),
DIST_DIR = __dirname,
HTML_FILE = path.join(DIST_DIR, '/src/index.html')
app.use(express.static(DIST_DIR))
app.get('*', (req, res) => {
res.sendFile(HTML_FILE)
})
// session support is required to use ExpressOIDC
app.use(session({
secret: process.env.RANDOM_SECRET_WORD,
resave: true,
saveUninitialized: false
}));
const oidc = new ExpressOIDC({
issuer: `${process.env.OKTA_ORG_URL}/oauth2/default`,
client_id: process.env.OKTA_CLIENT_ID,
client_secret: process.env.OKTA_CLIENT_SECRET,
redirect_uri: process.env.REDIRECT_URL,
scope: 'openid profile',
routes: {
callback: {
path: '/authorization-code/callback',
defaultRedirect: '/admin'
}
}
});
// ExpressOIDC will attach handlers for the /login and /authorization-code/callback routes
app.use(oidc.router);
app.use(cors());
app.use(bodyParser.json());
app.use(express.static(path.join(__dirname, 'src')));
app.get('/home', (req, res) => {
res.sendFile(path.join(__dirname, './home.html'));
});
app.get('/admin', oidc.ensureAuthenticated(), (req, res) => {
res.sendFile(path.join(__dirname, './admin.html'));
});
app.get('/logout', (req, res) => {
req.logout();
res.redirect('/home');
});
console.log("redirect");
app.get('/', (req, res) => {
res.redirect('/home');
});
// //for each blog post
app.get('/lifestyle/:title', function (req, res) {
res.send('Blogpost template here!')
})
const database = new Sequelize({
dialect: 'sqlite',
storage: './db.sqlite',
operatorsAliases: false,
});
const Post = database.define('posts', {
title: Sequelize.STRING,
content: Sequelize.TEXT,
});
const blogPost = database.define('blogposts', {
title: Sequelize.STRING,
content: Sequelize.TEXT,
published: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: false,
},
});
const test = database.define('test', {
title: Sequelize.STRING,
content: Sequelize.TEXT,
});
epilogue.initialize({ app, sequelize: database });
const PostResource = epilogue.resource({
model: Post,
endpoints: ['/posts', '/posts/:id'],
});
const blogPostResource = epilogue.resource({
model: blogPost,
endpoints: ['/blogposts', '/blogposts/:id'],
});
PostResource.all.auth(function (req, res, context) {
return new Promise(function (resolve, reject) {
resolve(context.continue);
})
});
database.sync().then(() => {
oidc.on('ready', () => {
app.listen(port, () => console.log(`My Blog App listening on port ${port}!`))
});
});
oidc.on('error', err => {
// An error occurred while setting up OIDC
console.log("oidc error: ", err);
});
my package.json:
{
"name": "blog",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"build": "rm -rf dist && webpack --mode development --config webpackserver.config.js && webpack --mode development",
"start": "node ./dist/server.js"
},
"proxy": "http://localhost:3000",
"author": "",
"license": "ISC",
"dependencies": {
"#okta/oidc-middleware": "1.0.2",
"D": "1.0.0",
"babel-polyfill": "6.26.0",
"babel-preset-es2015": "6.24.1",
"babel-preset-stage-0": "6.24.1",
"babel-runtime": "6.26.0",
"body-parser": "1.18.3",
"cors": "2.8.5",
"dotenv": "6.2.0",
"epilogue": "0.7.1",
"express": "4.16.4",
"express-session": "1.15.6",
"fibers": "4.0.2",
"node-sass": "4.13.0",
"rc": "1.2.8",
"sass": "1.23.7",
"sequelize": "4.44.3",
"sqlite3": "4.1.0",
"webpack-isomorphic-tools": "3.0.6"
},
"devDependencies": {
"#babel/core": "7.7.5",
"#babel/plugin-proposal-class-properties": "7.7.4",
"#babel/plugin-transform-runtime": "7.7.6",
"#babel/preset-env": "7.7.6",
"#babel/preset-react": "7.7.4",
"#material-ui/core": "4.7.2",
"#material-ui/icons": "4.5.1",
"babel-loader": "8.0.6",
"babel-plugin-transform-runtime": "6.23.0",
"babel-preset-env": "1.7.0",
"css-loader": "3.3.2",
"file-loader": "5.0.2",
"grommet": "2.9.0",
"html-loader": "0.5.5",
"html-webpack-plugin": "3.2.0",
"json-loader": "0.5.7",
"nodemon": "1.18.9",
"nodemon-webpack-plugin": "4.2.2",
"react": "16.12.0",
"react-dom": "16.12.0",
"react-router-dom": "5.1.2",
"sass-loader": "8.0.0",
"style-loader": "1.0.1",
"styled-components": "4.4.1",
"typeface-roboto": "0.0.75",
"webpack": "4.41.2",
"webpack-cli": "3.3.10",
"webpack-node-externals": "1.7.2"
}
}
my .babelrc"
{
"presets": [
"#babel/preset-env",
"#babel/react"
],
"plugins" : [
"#babel/plugin-proposal-class-properties",
"#babel/plugin-transform-runtime"
]
}

Test Jest React-Native Expo CRNA with Redux not ejected

How to get all tests working for Components, Redux Actions and Reducers for a Create React Native App (CRNA) using Expo (default) while not ejected?
Also uses Axios, Redux-Thunk async actions and React-Native Maps through Expo.
Well after reading and re-reading the relevant documentation for Jest, Enzyme and Redux,
as well as googling issues with specific NPM package versions I sorted this out.
There's a lot of "moving parts" in that all NPM packages have to play nice together. I.E testing, mocking, redux and and your flavour of React.
Here's what works at this time (2018-01-16).
Setup
Environment
OS X High Sierra
Visual Studio Code
Project platform
Create React Native App (CRNA)
Expo 23.0.4
React 16.0.0-alpha.12
React-Native 0.50.3
Testing framework
Jest ^22.0.6
Jest-CLI ^22.0.6
Jest-Enzyme ^4.0.2
Jest-Expo ^22.0.0
React-addons-test-utils ^15.6.2
React-DOM 16.0.0-beta.5
package.json
Working tests for Redux actions, reducers and components.
{
"name": "MyApp",
"version": "0.0.1",
"private": true,
"author": "Thomas Hagström <thomas#crossplatform.se>",
"devDependencies": {
"axios-mock-adapter": "^1.10.0",
"babel": "^6.3.26",
"babel-eslint": "^8.2.1",
"babel-jest": "^22.0.6",
"babel-polyfill": "^6.16.0",
"babel-preset-airbnb": "^1.0.1",
"babel-preset-es2015": "^6.18.0",
"babel-preset-react": "^6.16.0",
"babel-preset-react-native": "1.9.0",
"eslint": "^4.15.0",
"eslint-config-airbnb": "^16.1.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.5.1",
"jest": "^22.0.6",
"jest-cli": "^22.0.6",
"jest-enzyme": "^4.0.2",
"jest-expo": "^22.0.0",
"react-addons-test-utils": "^15.6.2",
"react-dom": "^16.0.0-beta.5",
"react-native-mock": "^0.3.1",
"react-native-scripts": "1.8.1",
"react-test-renderer": "^16.0.0-alpha.12",
"remotedev-rn-debugger": "^0.8.3"
},
"babel": {
"presets": [
"es2015",
"react"
]
},
"main": "./node_modules/react-native-scripts/build/bin/crna-entry.js",
"scripts": {
"start": "react-native-scripts start",
"eject": "react-native-scripts eject",
"android": "react-native-scripts android",
"ios": "react-native-scripts ios",
"test": "node node_modules/jest/bin/jest.js --watch",
"postinstall": "remotedev-debugger --hostname localhost --port 5678 --injectserver",
"eslint": "./node_modules/.bin/eslint"
},
"remotedev": {
"hostname": "localhost",
"port": 5678
},
"jest": {
"preset": "jest-expo",
"transformIgnorePatterns": [
"node_modules/(?!(react-native|jest-resolve|expo|lodash|enzyme|prop-types|react|jest-enzyme|enzyme|jest-expo|jest-serializer-enzyme|react-native-elements|react-native-google-places-autocomplete)/)"
],
"setupFiles": [
"./config/jest/globalFetch.js",
"./config/enzyme/index.js"
]
},
"dependencies": {
"#expo/vector-icons": "^6.2.2",
"axios": "^0.17.1",
"expo": "^23.0.4",
"enzyme": "^3.3.0",
"enzyme-adapter-react-16": "^1.1.1",
"lodash": "^4.17.4",
"prop-types": "^15.6.0",
"react": "16.0.0-alpha.12",
"react-native": "0.50.3",
"react-native-elements": "^0.18.5",
"react-native-google-places-autocomplete": "^1.3.6",
"react-native-maps": "^0.18.0",
"react-navigation": "^1.0.0-beta.23",
"react-navigation-redux": "^0.1.0",
"react-redux": "^5.0.6",
"redux": "^3.7.2",
"redux-logger": "^3.0.6",
"redux-promise": "^0.5.3",
"redux-thunk": "^2.2.0",
"redux-mock-store": "^1.4.0",
"remote-redux-devtools": "^0.5.12",
"socketcluster-server": "^9.1.2"
}
}
Enzyme global config
The config script for Enzyme, see package.json below, looks like this.
// config/enzyme/index.js
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
// Setup enzyme's react adapter
Enzyme.configure({ adapter: new Adapter() });
Examples
Global Mocks
Expo Kit
In the root of my project I've placed mocks in a __mocks__ directory so they will automatically be picked up by Jest.
This will solve cases where native mobile API calls are used - specifically ExpoKit SDK - and not just HTTP REST.
// __mocks__/expo.js
jest.mock('expo', () => {
const expo = require.requireActual('expo');
const positionMock = {
latitude: 1,
longitude: 1,
};
// Mock the expo library
return {
Location: {
setApiKey: jest.fn(),
getCurrentPositionAsync:
options =>
new Promise(
resolve => resolve(options ? {
coords: positionMock,
} : null)
, null,
)
,
},
Constants: {
manifest: {
extra: { google: { maps: 'Your-API-KEY-HERE' } },
},
},
Permissions: {
LOCATION: 'location',
askAsync: type => new Promise(resolve =>
resolve(type === 'location' ?
{ status: 'granted' }
: null)),
},
...expo,
};
});
Redux - Mock - Store
To configure Redux with Thunk, so you don't have to do this before every (action) test. Meaning in your tests importing redux-mock-store will use the below implementation:
// __mocks__/redux-mock-store.js
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
export default mockStore;
Constants
Used as redux action types.
// src/Constants.js
const MapConstants = {
MAP_LOCATION_CHANGED: 'MAP REGION CHANGED',
MAP_LOCATION_BUSY: 'MAP: GETTING LOCATION',
MAP_LOCATION_SUCCESS: 'MAP: GET LOCATION SUCCESS',
MAP_LOCATION_FAILED: 'MAP: GET LOCATION FAILED',
};
Redux Action Creators
Here we used the above configuration in an action test.
// src/Actions/__tests__/MapActions.test.js
import configureMockStore from 'redux-mock-store';
import { MapConstants } from '../../Constants';
import {
GetLocation
} from '../MapActions';
const store = configureMockStore();
describe('map actions', () => {
beforeEach(() => {
store.clearActions();
});
test('GetLocation returns SUCCESS when done', async () => {
const expectedPayload = { latitude: 1, longitude: 1 };
const expectedActions = [
{ type: MapConstants.MAP_LOCATION_BUSY },
{ type: MapConstants.MAP_LOCATION_SUCCESS, payload: expectedPayload },
];
// Dispatch action
await store.dispatch(GetLocation());
expect(store.getActions()).toMatchSnapshot();
expect(store.getActions()).toEqual(expectedActions);
});
});
Components
I use a pure component and do my redux connect on a separate container.
import React from 'react';
import { shallow } from 'enzyme';
import Map from '../Map';
import { Colors } from '../../styles';
// src/Components/__tests__/map.test.js
function setup () {
const props = {
GetLocation: jest.fn(),
LocationChanged: jest.fn(),
map: {
isBusy: false,
hasError: false,
errorMessage: null,
location: null,
region: {
latitude: 45.52220671242907,
longitude: -122.6653281029795,
latitudeDelta: 0.04864195044303443,
longitudeDelta: 0.040142817690068,
},
},
};
const enzymeWrapper = shallow(<Map {...props} />);
return {
props,
enzymeWrapper,
};
}
describe('components', () => {
describe('Map', () => {
it('should render self and subcomponents', () => {
const { enzymeWrapper } = setup();
expect(enzymeWrapper).toMatchSnapshot();
const busyProps = enzymeWrapper.find('BusyIndicator').props();
expect(busyProps.isBusy).toBe(false);
expect(busyProps.loadingIndicatorColor).toEqual("#FFFFFF");
});
// TODO: Mock map functions
});
});
Redux Reducer
Ensure the reducer returns correct state and doesn't mutate it.
import MapReducer from '../MapReducer';
import { MapConstants } from '../../Constants';
describe('MapReducer', () => {
test('should return the initial state', () => {
expect(MapReducer(undefined, {}))
.toEqual({
isBusy: false,
hasError: false,
errorMessage: null,
location: null,
region: {
latitude: 45.52220671242907,
longitude: -122.6653281029795,
latitudeDelta: 0.04864195044303443,
longitudeDelta: 0.040142817690068,
},
});
});
test(`should handle ${MapConstants.MAP_LOCATION_BUSY}`, () => {
expect(MapReducer({}, {
type: MapConstants.MAP_LOCATION_BUSY,
}))
.toEqual({
hasError: false,
errorMessage: null,
isBusy: true,
type: MapConstants.MAP_LOCATION_BUSY,
});
});
test(`should handle ${MapConstants.MAP_LOCATION_SUCCESS}`, () => {
const resultArray = ['test'];
expect(MapReducer({}, {
type: MapConstants.MAP_LOCATION_SUCCESS,
payload: resultArray,
}))
.toEqual({
isBusy: false,
hasError: false,
errorMessage: null,
location: resultArray,
type: MapConstants.MAP_LOCATION_SUCCESS,
});
});
test(`should handle ${MapConstants.MAP_LOCATION_FAILED}`, () => {
const errorString = 'test error';
expect(MapReducer({}, {
type: MapConstants.MAP_LOCATION_FAILED,
payload: errorString,
}))
.toEqual({
isBusy: false,
hasError: true,
errorMessage: errorString,
location: null,
type: MapConstants.MAP_LOCATION_FAILED,
});
});
test(`should handle ${MapConstants.MAP_LOCATION_CHANGED}`, () => {
const resultArray = ['test'];
expect(MapReducer({}, {
type: MapConstants.MAP_LOCATION_CHANGED,
payload: resultArray,
}))
.toEqual({
isBusy: false,
hasError: false,
errorMessage: null,
region: resultArray,
type: MapConstants.MAP_LOCATION_CHANGED,
});
});
});