Tag: Application

Creating Dynamic Routes in a Nuxt Application

In this post, we’ll be using an ecommerce store demo I built and deployed to Netlify to show how we can make dynamic routes for incoming data. It’s a fairly common use-case: you get data from an API, and you either don’t know exactly what that data might be, there’s a lot of it, or it might change. Luckily for us, Nuxt makes the process of creating dynamic routing very seamless.

In the last post, we showed how to set up stripe payments with Netlify Functions, which allow us to create serverless lambda functions with ease. We’ll use this same application to show another Nuxt-specific functionality as well.

Let’s get started!

Creating the page

In this case, we’ve got some dummy data for the store that I created in mockaroo and am storing in the static folder. Typically you’ll use fetch or axios and an action in the Vuex store to gather that data. Either way, we store the data with Vuex in store/index.js, along with the UI state, and an empty array for the cart.

import data from '~/static/storedata.json'  export const state = () => ({  cartUIStatus: 'idle',  storedata: data,  cart: [] })

It’s important to mention that in Nuxt, all we have to do to set up routing in the application is create a .vue file in the pages directory. So we have an index.vue page for our homepage, a cart.vue page for our cart, and so on. Nuxt automagically generates all the routing for these pages for us.

In order to create dynamic routing, we will make a directory to house those pages. In this case, I made a directory called /products, since that’s what the routes will be, a view of each individual product details.

In that directory, I’ll create a page with an underscore, and the unique indicator I want to use per page to create the routes. If we look at the data I have in my cart, it looks like this:

[  {    "id": "9d436e98-1dc9-4f21-9587-76d4c0255e33",    "color": "Goldenrod",    "description": "Mauris enim leo, rhoncus sed, vestibulum sit amet, cursus id, turpis. Integer aliquet, massa id lobortis convallis, tortor risus dapibus augue, vel accumsan tellus nisi eu orci. Mauris lacinia sapien quis libero.",    "gender": "Male",    "name": "Desi Ada",    "review": "productize virtual markets",    "starrating": 3,    "price": 50.40,    "img": "1.jpg"  },   … ]

You can see that the ID for each entry is unique, so that’s a good candidate for something to use, we’ll call the page:

_id.vue

Now, we can store the id of the particular page in our data by using the route params:

data() {  return {    id: this.$  route.params.id,   } },

For the entry from above, our data if we looked in devtools would be:

id: "9d436e98-1dc9-4f21-9587-76d4c0255e33"

We can now use this to retrieve all of the other information for this entry from the store. I’ll use mapState:

import { mapState } from "vuex";  computed: {  ...mapState(["storedata"]),  product() {    return this.storedata.find(el => el.id === this.id);  } },

And we’re filtering the storedata to find the entry with our unique ID!

Let the Nuxt config know

If we were building an app using yarn build, we’d be done, but we’re using Nuxt to create a static site. When we use Nuxt to create a static site, we’ll use the yarn generate command. We have to let Nuxt know about the dynamic files with the generate command in nuxt.config.js.

This command will expect a function that will return a promise that resolves in an array that will look like this:

export default {   generate: {     routes: [       '/product/1',       '/product/2',       '/product/3'     ]   } }

To create this, at the top of the file we’ll bring in the data from the static directory, and create the function:

import data from './static/storedata.json' let dynamicRoutes = () => {  return new Promise(resolve => {    resolve(data.map(el => `product/$  {el.id}`))  }) }

We’ll then call the function within our config:

generate: {   routes: dynamicRoutes },

If you’re gathering your data from an API with axios instead (which is more common), it would look more like this:

import axios from 'axios' let dynamicRoutes = () => {  return axios.get('https://your-api-here/products').then(res => {    return res.data.map(product => `/product/$  {product.id}`)  }) }

And with that, we’re completely done with the dynamic routing! If you shut down and restart the server, you’ll see the dynamic routes per product in action!

For the last bit of this post, we’ll keep going, showing how the rest of the page was made and how we’re adding items to our cart, since that might be something you want to learn, too.

Populate the page

Now we can populate the page with whatever information we want to show, with whatever formatting we would like, as we have access to it all with the product computed property:

<main>  <section class="img">    <img :src="`/products/$  {product.img}`" />  </section>  <section class="product-info">    <h1>{{ product.name }}</h1>    <h4 class="price">{{ product.price | dollar }}</h4>    <p>{{ product.description }}</p>  </section>  ... </main>

In our case, we’ll also want to add items to the cart that’s in the store. We’ll add the ability to add and remove items (while not letting the decrease count dip below zero

<p class="quantity">  <button class="update-num" @click="quantity > 0 ? quantity-- : quantity = 0">-</button>  <input type="number" v-model="quantity" />  <button class="update-num" @click="quantity++">+</button> </p> ... <button class="button purchase" @click="cartAdd">Add to Cart</button>

In our methods on that component, we’ll add the item plus a new field, the quantity, to an array that we’ll pass as the payload to mutation in the store.

methods: {  cartAdd() {    let item = this.product;    item.quantity = this.quantity;    this.tempcart.push(item);    this.$  store.commit("addToCart", item);  } }

In the Vuex store, we’ll check if the item already exists. If it does, we’ll just increase the quantity. If not, we’ll add the whole item with quantity to the cart array.

addToCart: (state, payload) => {  let itemfound = false  state.cart.forEach(el => {    if (el.id === payload.id) {      el.quantity += payload.quantity      itemfound = true    }  })  if (!itemfound) state.cart.push(payload) }

We can now use a getter in the store to calculate the total, which is what we’ll eventually pass to our Stripe serverless function (the other post picks up from here). We’ll use a reduce for this as reduce is very good at retrieving one value from many. (I wrote up more details on how reduce works here).

cartTotal: state => {  if (!state.cart.length) return 0  return state.cart.reduce((ac, next) => ac + next.quantity * next.price, 0) }

And there you have it! We’ve set up individual product pages, and Nuxt generates all of our individual routes for us at build time. You’d be Nuxt not to try it yourself. 😬

The post Creating Dynamic Routes in a Nuxt Application appeared first on CSS-Tricks.

CSS-Tricks

, , , ,

Using Cypress to Write Tests for a React Application

End-to-end tests are written to assert the flow of an application from start to finish. Instead of handling the tests yourself — you know, manually clicking all over the application — you can write a test that runs as you build the application. That’s what we call continuous integration and it’s a beautiful thing. Write some code, save it, and let tooling do the dirty work of making sure it doesn’t break anything.

>Cypress is just one end-to-end testing framework that does all that clicking work for us and that’s what we’re going to look at in this post. It’s really for any modern JavaScript library, but we’re going to integrate it with React in the examples.

Let’s set up an app to test

In this tutorial, we will write tests to cover a todo application I’ve built. You can clone the repository to follow along as we plug it into Cypress.

git clone git@github.com:kinsomicrote/cypress-react-tutorial.git

Navigate into the application, and install the dependencies:

cd cypress-react-tutorial yarn install

Cypress isn’t part of the dependencies, but you can install it by running this:

yarn add cypress --dev

Now, run this command to open Cypress:

node_modules/.bin/cypress open

Typing that command to the terminal over and over can get exhausting, but you can add this script to the package.json file in the project root:

"cypress": "cypress open"

Now, all you have to do is do npm run cypress once and Cypress will be standing by at all times. To have a feel of what the application we’ll be testing looks like, you can start the React application by running yarn start.

We will start by writing a test to confirm that Cypress works. In the cypress/integration folder, create a new file called init.spec.js. The test asserts that true is equal to true. We only need it to confirm that’s working to ensure that Cypress is up and running for the entire application.

describe('Cypress', () => {   it('is working', () => {     expect(true).to.equal(true)   }) })

You should have a list of tests open. Go there and select init.spec.js.

That should cause the test to run and pop up a screen that shows the test passing.

While we’re still in init.spec.js, let’s add a test to assert that we can visit the app by hitting http://localhost:3000 in the browser. This’ll make sure the app itself is running.

it('visits the app', () => {   cy.visit('http://localhost:3000') })

We call the method visit() and we pass it the URL of the app. We have access to a global object called cy for calling the methods available to us on Cypress.

To avoid having to write the URL time and again, we can set a base URL that can be used throughout the tests we write. Open the cypress.json file in the home directory of the application and add define the URL there:

{   "baseUrl": "http://localhost:3000" }

You can change the test block to look like this:

it('visits the app', () => {   cy.visit('/') })

…and the test should continue to pass. 🤞

Testing form controls and inputs

The test we’ll be writing will cover how users interact with the todo application. For example, we want to ensure the input is in focus when the app loads so users can start entering tasks immediately. We also want to ensure that there’s a default task in there so the list is not empty by default. When there are no tasks, we want to show text that tells the user as much.

To get started, go ahead and create a new file in the integration folder called form.spec.js. The name of the file isn’t all that important. We’re prepending “form” because what we’re testing is ultimately a form input. You may want to call it something different depending on how you plan on organizing tests.

We’re going to add a describe block to the file:

describe('Form', () => {   beforeEach(() => {     cy.visit('/')   })    it('it focuses the input', () => {     cy.focused().should('have.class', 'form-control')   }) })

The beforeEach block is used to avoid unnecessary repetition. For each block of test, we need to visit the application. It would be redundant to repeat that line each time beforeEach ensures Cypress visits the application in each case.

For the test, let’s check that the DOM element in focus when application first loads has a class of form-control. If you check the source file, you will see that the input element has a class called form-control set to it, and we have autoFocus as one of the element attributes:

<input   type="text"   autoFocus   value={this.state.item}   onChange={this.handleInputChange}   placeholder="Enter a task"   className="form-control" />

When you save that, go back to the test screen and select form.spec.js to run the test.

The next thing we’ll do is test whether a user can successfully enter a value into the input field.

it('accepts input', () => {   const input = "Learn about Cypress"   cy.get('.form-control')     .type(input)     .should('have.value', input) })

We’ve added some text (“Learn about Cypress”) to the input. Then we make use of cy.get to obtain the DOM element with the form-control class name. We could also do something like cy.get('input') and get the same result. After getting the element, cy.type() is used to enter the value we assigned to the input, then we assert that the DOM element with class form-control has a value that matches the value of input.

In other words:

Does the input have a class of form-control and does it contain Learn About Cypress in it?

Our application should also have two todos that have been created by default when the app runs. It’s important we have a test that checks that they are indeed listed.

What do we want? In our code, we are making use of the list item (<li>) element to display tasks as items in a list. Since we have two items listed by default, it means that the list should have a length of two at start. So, the test will look something like this:

it('displays list of todo', () => {   cy.get('li')     .should('have.length', 2) })

Oh! And what would this app be if a user was unable to add a new task to the list? We’d better test that as well.

it('adds a new todo', () => {   const input = "Learn about cypress"   cy.get('.form-control')     .type(input)     .type('{enter}')     .get('li')     .should('have.length', 3) })

This looks similar to what we wrote in the last two tests. We obtain the input and simulate typing a value into it. Then, we simulate submitting a task that should update the state of the application, thereby increasing the length from 2 to 3. So, really, we can build off of what we already have!

Changing the value from three to two will cause the test to fail — that’s what we’d expect because the list should have two tasks by default and submitting once should produce a total of three.

You might be wondering what would happen if the user deletes either (or both) of the default tasks before attempting to submit a new task. Well, we could write a test for that as well, but we’re not making that assumption in this example since we only want to confirm that tasks can be submitted. This is an easy way for us to test the basic submitting functionality as we develop and we can account for advanced/edge cases later.

The last feature we need to test is the deleting tasks. First, we want to delete one of the default task items and then see if there is one remaining once the deletion happens. It’s the same sort of deal as before, but we should expect one item left in the list instead of the three we expected when adding a new task to the list.

it('deletes a todo', () => {   cy.get('li')     .first()     .find('.btn-danger')     .click()     .get('li')     .should('have.length', 1) })

OK, so what happens if we delete both of the default tasks in the list and the list is completely empty? Let’s say we want to display this text when no more items are in the list: “All of your tasks are complete. Nicely done!”

This isn’t too different from what we have done before. You can try it out first then come back to see the code for it.

it.only('deletes all todo', () => {   cy.get('li')     .first()     .find('.btn-danger')     .click()     .get('li')     .first()     .find('.btn-danger')     .click()     .get('.no-task')     .should('have.text', 'All of your tasks are complete. Nicely done!') })

Both tests look similar: we get the list item element, target the first one, and make use of cy.find() to look for the DOM element with a btn-danger class name (which, again, is a totally arbitrary class name for the delete button in this example app). We simulate a click event on the element to delete the task item.

I’m checking for “No task!” in this particular test.

Testing network requests

Network requests are kind of a big deal because that’s often the source of data used in an application. Say we have a component in our app that makes a request to the server to obtain data which will be displayed to user. Let’s say the component markup looks like this:

class App extends React.Component {   state = {     isLoading: true,     users: [],     error: null   };   fetchUsers() {     fetch(`https://jsonplaceholder.typicode.com/users`)       .then(response => response.json())       .then(data =>         this.setState({           users: data,           isLoading: false,         })       )       .catch(error => this.setState({ error, isLoading: false }));   }   componentDidMount() {     this.fetchUsers();   }   render() {     const { isLoading, users, error } = this.state;     return (       <React.Fragment>         <h1>Random User</h1>         {error ? <p>{error.message}</p> : null}         {!isLoading ? (           users.map(user => {             const { username, name, email } = user;             return (               <div key={username}>                 <p>Name: {name}</p>                 <p>Email Address: {email}</p>                 <hr />               </div>             );           })         ) : (           <h3>Loading...</h3>         )}       </React.Fragment>     );   } }

Here, we are making use of the JSON Placeholder API as an example. We can have a test like this to test the response we get from the server:

describe('Request', () => {   it('displays random users from API', () => {     cy.request('https://jsonplaceholder.typicode.com/users')       .should((response) => {         expect(response.status).to.eq(200)         expect(response.body).to.have.length(10)         expect(response).to.have.property('headers')         expect(response).to.have.property('duration')       })   }) })

The benefit of testing the server (as opposed to stubbing it) is that we are certain the response we get is the same as that which a user will get. To learn more about network requests and how you can stub network requests, see this page in the Cypress documentation.

Running tests from the command line

Cypress tests can run from the terminal without the provided UI:

./node_modules/.bin/cypress run

…or

npx cypress run

Let’s run the form tests we wrote:

npx cypress run --record --spec "cypress/integration/form.spec.js"

Terminal should output the results right there with a summary of what was tested.

There’s a lot more about using Cypress with the command line in the documentation.

That’s a wrap!

Tests are something that either gets people excited or scared, depending on who you talk to. Hopefully what we’ve looked at in this post gets everyone excited about implementing tests in an application and shows how relatively straightforward it can be. Cypress is an excellent tool and one I’ve found myself reaching for in my own work, but there are others as well. Regardless of what tool you use (and how you feel about tests), hopefully you see the benefits of testing and are more compelled to give them a try.

Related resources

The post Using Cypress to Write Tests for a React Application appeared first on CSS-Tricks.

CSS-Tricks

, , , , ,
[Top]

Application Holotypes

It’s entirely too common to make broad-sweeping statements about all websites. Jason Miller:

We often make generalizations about applications we see in the wild, both anecdotal and statistical: “Single-Page Applications are slower than multipage” or “apps with low TTI loaded fast”. However, the extent to which these generalizations hold for the performance and architectural characteristics we care about varies.

Just the other morning, at breakfast an An Event Apart, I sat with a fellow who worked on a university website with a massive amount of pages. Also at the table was someone who worked at a media company with a wide swath of brands, but all largely sites with blog-like content. There was also someone who worked on a developer tool that was heavy on dashboards. We can all care about accessibility, performance, maintainability, etc., but the appropriate technology stacks and delivery processes are quite different both in what we actually do and what we probably should do.

It’s a common stab at solving for these different sites by making two buckets: web sites and web apps. Or dynamic sites and static sites. Or content sites and everything else. Jason builds us more buckets (“holotypes”): (more…)

,
[Top]