visit
git clone //github.com/beatfactor/middlemarch
npm install
npm run dev
create-vite
Scaffolding Tool
The ‘create-vite’ tool will also create the project folder for you, so just make sure to cd into the parent folder first: cd ~/workspace
Install Vite
and initialise the project with:
npm init vite@latest
From the list, choose vue
:
~/workspace % npm init vite@latest
npx: installed 6 in 1.051s
✔ Project name: … vue-bookstore
? Select a framework: › - Use arrow-keys. Return to submit.
vanilla
❯ vue
react
preact
lit
svelte
Then select vue
as the variant, since we’ll not be using TypeScript:
? Select a variant: › - Use arrow-keys. Return to submit.
❯ vue
vue-ts
npx: installed 6 in 1.051s
✔ Project name: … vue-bookstore
✔ Select a framework: › vue
✔ Select a variant: › vue
Scaffolding project in /Users/andrei/workspace/vue-bookstore...
Done. Now run:
cd vue-bookstore
npm install
npm run dev
vite v2.7.7 dev server running at:
> Local: //localhost:3000/
> Network: use `--host` to expose
ready in 611ms.
Let’s review the project’s directory structure created by the create-vite
tool:
vue-bookstore/
├── public/
| ├── favicon.ico
├── src/
| ├── assets/
| | └── logo.png
| ├── components/
| | └── HelloWorld.vue
| ├── App.vue
| └── main.js
├─── package.json
├─── README.md
└─── vite.config.js
In this section of our guide, we’ll be adding two new dependencies to our project: Vue-router and pinia. Let’s go ahead and install them from NPM.
npm install vue-router@4 --save
npm install pinia --save
The scaffolding created using the create-vite
tool adds a very basic Vue component, located in src/components/HelloWorld.vue
. It is then used in the main application component, located in src/App.vue
.
index.html
src/main.js
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
src/main.js
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
Homepage - our bookstore homepage
Cart - the shopping cart and check out page
Sign-In - the user sign-in page
src/
├── components/
| └── TopNavbar.js
├── lib/
| ├── router.js
| └── store.js
├── pages/
| ├── cart/
| | ├── cart.css
| | ├── cart.html
| | └── Cart.vue
| ├── home/
| | ├── home.css
| | ├── home.html
| | └── Home.vue
| ├── sign-in/
| | ├── sign-in.css
| | ├── sign-in.html
| | └── SignIn.vue
| └── routes.js
├── App.vue
└── main.js
We’ve added three pages, each of which we’ll keep very basic. We’ll just add TobNavbar
components to make the navigation work without page reloads.
Add the following for src/pages/cart/Cart.vue
, src/pages/home/Home.vue
and src/pages/sign-in/SignIn.vue
:
<script setup>
import TopNavbar from '../../components/TopNavbar.vue';
</script>
<template>
<TopNavbar />
</template>
<style></style>
<script>
export default {
components: {
TopNavbar
},
computed: {},
mounted() {
},
data() {
return {
};
},
};
</script>
The TopNavbar
component located in src/components
will contain just the navigation links.
Notice how the router-link component which is part of the vue-router
appears:
<template>
<router-link to="/">Home</router-link>
<router-link to="/cart/">Cart</router-link>
<router-link to="/sign-in/">Sign In</router-link>
</template>
The pages/routes.js
file contains all the route declarations for the application. Here’s how it looks:
import {createRouter} from 'vue-router'
import Homepage from './home/Home.vue';
import SignIn from './sign-in/SignIn.vue';
import Cart from './cart/Cart.vue';
const routes = [
{
path: '/',
component: Homepage
},
{
path: '/sign-in/',
component: SignIn
},
{
path: '/cart/',
component: Cart
},
]
export default function (history) {
return createRouter({
history,
routes
})
}
Before we’re ready to see the vue-router
in action, we need to do 2 more things:
1) Create the router and add it to the main Vue application instance, in src/main.js
:
import { createApp } from 'vue'
import { createWebHistory } from 'vue-router'
import createRouter from './pages/routes.js'
import App from './App.vue'
const router = createRouter(createWebHistory())
const app = createApp(App)
app.use(router).mount('#app')
2) Add the <router-view>
component in src/App.vue
:
<template>
<router-view></router-view>
</template>
Now re-run npm run dev
if needed and then navigate to //localhost:3000
where you’ll have a routing-enabled Vue 3 app.
We’ll do that in src/main.js
and update it to look like:
import { createApp } from 'vue'
import { createWebHistory } from 'vue-router'
import { createPinia } from 'pinia'
import createRouter from './pages/routes.js'
import App from './App.vue'
const store = createPinia()
const router = createRouter(createWebHistory())
const app = createApp(App)
app.use(router).use(store).mount('#app')
Like Vuex, the Pinia store contains the state and two types of methods: getters and actions.
Some things to consider about a store:
Getters
are synchronous functions used to retrieve data from the stateActions
are functions that can also be asynchronous which are used to update the statestate
is defined as a function returning the initial state
It’s time now to create the catalog store inside src/stores/catalog.js
:
import { defineStore } from 'pinia'
export const useCatalog = defineStore('catalog-store', {
state: () => {
return {
newArrivals: [],
fetching: false
}
},
getters: {
results(state) {
return state.newArrivals;
},
isFetching(state) {
return state.fetching;
}
},
actions: {
async fetchNewArrivals() {
this.fetching = true;
const response = await fetch('/data/new-arrivals.json');
try {
const result = await response.json();
this.newArrivals = result.books;
} catch (err) {
this.newArrivals = [];
console.error('Error loading new arrivals:', err);
return err;
}
this.fetching = false;
}
}
})
Looking at the above source code, you’ll notice we have two ‘getters’ (results
and isFetching
) and one action (fetchNewArrivals
). Instead of a real backend, we have just a json file located in /data/new-arrivals.json
which contains a few books that we’ll use as our catalog.
Let’s create a new component called NewArrivals
inside src/components/NewArrivals.vue
which we’ll use in the Home.vue
page component.
<script setup>
import {useCatalog} from '../../store/catalog.js'
</script>
<template>
</template>
<style scoped></style>
<script>
import { mapState, mapActions } from 'pinia'
export default {
computed: {
...mapState(useCatalog, {newArrivals: 'results'})
},
methods: {
...mapActions(useCatalog, ['fetchNewArrivals']),
addToCart() {
// we'll populate this later
}
},
created() {
// when the template is created, we call this action
this.fetchNewArrivals();
}
};
</script>
The Home.vue
component becomes:
<script setup>
import TopNavbar from '../../components/TopNavbar.vue';
import NewArrivals from '../../components/NewArrivals.vue';
</script>
<template>
<TopNavbar />
<NewArrivals />
</template>
<style></style>
<script>
export default {
components: {
TopNavbar,
NewArrivals
},
computed: {},
mounted() {},
data() {
return {};
},
};
</script>
I also wrote a store and a component for the cart but I will not include it in the tutorial. The mechanism is similar and you can inspect the source code in the repository which has everything included, even some extra styles.
npm install @vue/test-utils@next --save-dev
npm install nightwatch--save-dev
And we’ll also need the vite-plugin-nightwatch
mentioned earlier:
npm install vite-plugin-nightwatch --save-dev
Nightwatch uses the for browser automation tasks and we’ll need to install the chromedriver
NPM package as well, because we’re going to use Chrome to run our tests.
npm install chromedriver --save-dev
The vite-plugin-nightwatch
mentioned earlier includes a test renderer page and Nightwatch already contains everything needed for running the initial test for our component.
Create a folder test
and inside it two subfolders:
component
- this will hold component testse2e
- this will hold end-to-end tests
We also need a nightwatch.conf.js
configuration file, but we can run Nightwatch directly and have the config file created for us automatically. Just make sure chromedriver
is also installed (and the Chrome browser, of course).
Make sure the current working directory is the project root and then simply run an that is bundled with Nightwatch. We’ll pick the duckDuckGo
test because it’s the fastest:
$ npx nightwatch examples/tests/duckDuckGo.js
vue-bookstore/
├── public/
| ├── data/
| └── favicon.ico
├── src/
├── ...
| └── main.js
├── test/
| ├── component/
| └── e2e/
├─── nightwatch.conf.js
├─── package.json
├─── README.md
└─── vite.config.js
We’ll go ahead and create a new file called newArrivalsTest.js
inside test/component
. In it, we’ll add a basic test that mounts the component and checks if the returned element can be found on the page (i.e. the component has been mounted).
describe('New Arrivals Component Test', function() {
it('checks if the component has been mounted', async (browser) => {
const component = await browser.mountVueComponent('/src/components/new-arrivals/NewArrivals.vue', {
plugins: {
router: '/src/lib/router.js'
}
})
expect(component).to.be.present;
});
});
Nightwatch uses the same describe()
syntax as Mocha. You can even use Mocha as a test runner if you’re already familiar with it, but we’re not going to do that for now. In case you’d like to use Mocha, you only need to slip a few switches in the Nightwatch config file and there’s documentation available on the on how to do that.
npx nightwatch test/component/newArrivalsTest.js --env chrome
This will open the Chrome browser and render the component, then perform the test. If you don’t like seeing the browser window pop up during the test, you can pass the --headless
the argument, like so:
npx nightwatch test/component/newArrivalsTest.js --env chrome --headless
[New Arrivals Component Test] Test Suite
──────────────────────────────────────────────────────────────
ℹ Connected to ChromeDriver on port 9515 (652ms).
Using: chrome (97.0.4692.99) on MAC OS X.
Running tests the component:
──────────────────────────────────────────────────────────────
✔ Expected element <web element{e53f9b1e-11d3-4dc4-8728-4d3cd077343e}> to be present (1ms)
OK. 1 assertions passed. (781ms)
npx nightwatch --help
We’ll just inspect the NewArrivals
component and check if there is a property in it called newArrivals
, which is used in the HTML to render the results.
The test looks like this now. We’ve refactored the component mounting into the before
hook so we can only do the checks inside the test, the it
block. The expect
library is provided by Nightwatch out of the box and it is based on the popular and versatile assertion library.
More info on how to use the expect
on the website.
describe('New Arrivals Component Test', function() {
let component;
before(async () => {
component = await browser.mountVueComponent('/src/components/new-arrivals/NewArrivals.vue', {
plugins: {
router: '/src/lib/router.js'
}
})
});
it('checks if the component has been mounted', function(browser) {
expect(component).to.be.present;
expect(component).to.have.property('newArrivals');
expect(component).text.toContain('The Memory Police')
expect.elements('div.col-md-6').count.toEqual(4); expect(component.property('newArrivals')).to.be.an('array').with.length(1);
});
});
Let’s get started with the homepage end-to-end test and create a new file under test/e2e/homePageTest.js
. The syntax is the same as for the component test, but for running the end-to-end tests we’ll use the compiled build of our application.
To run the production build we have two options and each of them involves running a Vite
command, which is wrapped in NPM tasks.
npm run build
- this will generate the index.html and the other static assets. You can use this option if you already have a local webserver set up.npm run preview
- this will generate a production build and run it using the built-in dev server, by default at //localhost:5000
.
The second option is clearly more straightforward and so let’s just run the preview
command and see what happens:
$ npm run preview
> [email protected] preview /Users/andrei/workspace/vue-bookstore
> vite preview
> Local: //localhost:5000/
> Network: use `--host` to expose
Now that we have a production-ready build running, we can start writing the actual test in test/e2e/homePageTest.js
.
describe('Homepage End-to-end Test', () => {
it('tests if homepage is loaded', browser => {
browser
.navigateTo('//localhost:3000')
.assert.visible('#app .new-arrivals-panel')
.expect.elements('#app .new-arrivals-panel .col-md-6').count.toEqual(4)
});
it('adds 2 volumes of "Rhinoceros and Other Plays" to cart', browser => {
browser
.click('.new-arrivals-panel .col-md-6:nth-child(2) button.add-to-cart')
.click('.new-arrivals-panel .col-md-6:nth-child(2) button.add-to-cart')
.assert.textEquals('.shopping-cart .badge', '2');
});
after(browser => browser.end());
});
npx nightwatch test/e2e/homePageTest.js --env chrome
[Homepage End-to-end Test] Test Suite
──────────────────────────────────────────────────────────────
ℹ Connected to ChromeDriver on port 9515 (2454ms).
Using: chrome (97.0.4692.99) on MAC OS X.
Running tests the homepage:
──────────────────────────────────────────────────────────────
✔ Testing if element <#app .new-arrivals-panel> is visible (157ms)
✔ Expected elements <#app .new-arrivals-panel .col-md-6> count to equal: "4" (18ms)
OK. 2 assertions passed. (765ms)
npm i geckodriver --save-dev
npx nightwatch test/e2e/homePageTest.js --env firefox
[Homepage End-to-end Test] Test Suite
──────────────────────────────────────────────────────────────
ℹ Connected to GeckoDriver on port 4444 (1737ms).
Using: firefox (96.0.2) on MAC (20.6.0).
Running tests the homepage:
──────────────────────────────────────────────────────────────
✔ Testing if element <#app .new-arrivals-panel> is visible (54ms)
✔ Expected elements <#app .new-arrivals-panel .col-md-6> count to equal: "4" (6ms)
OK. 2 assertions passed. (612ms)
If you’re using a Mac, then safaridriver
is probably already installed, depending on your Safari version.
safaridriver --help
Usage: safaridriver [options]
-h, --help Prints out this usage information.
--version Prints out version information and exits.
-p, --port Port number the driver should use. If the server is already running, the port cannot be changed. If port 0 is specified, a default port will be used.
--enable Applies configuration changes so that subsequent WebDriver sessions will run without further authentication.
--diagnose Causes safaridriver to log diagnostic information for all sessions hosted by this instance. See the safaridriver(1) man page for more details about diagnostic logging.
safaridriver --enable
npx nightwatch test/e2e/homePageTest.js --env safari
npx nightwatch test/e2e/homePageTest.js --env firefox,chrome
npx nightwatch test/e2e/homePageTest.js --env firefox,chrome,safari
Nightwatch also supports running tests in parallel by dividing the total number of test script files over a configurable number of workers. But since we only have one file for now, we’ll skip this part. More on parallelism on the website.
It looks like it’s time to wrap things up and put everything together. Before we can enable continuous deployment in Github Actions, we need to create the test
NPM task.
We’ll add that as a new NPM task called test
so let’s edit the package.json
and add the following, inside the “scripts” dictionary:
{
"test": "nightwatch ./test"
}
npm test -- --env chrome --headless
We’ll use --headless
mode in order to run the tests in Github Actions.
Creating the Github Actions workflow means adding a new file called node.js.yml in the .github/workflows
folder which should look like below. Most of this is auto-generated when you navigate to the Actions section from your Github project and choose the Node.js template.
name: Node.js CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12.x, 14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- name: Start vite dev server
run: npm run dev &
- name: Build the app
run: npm run build
- name: Start vite dev server in preview
run: npm run preview &
- name: Run Nightwatch tests
run: npm test
And that's it. A new build will run for each new git push or whenever a new pull request is sent. The build will be run in 2 separate environments, one for Node 12 and the other Node 14, as defined in the workflow definition.
You can get it running on your local machine with the usual steps:
git clone //github.com/beatfactor/middlemarch
npm install
npm run dev
Feel free to send pull requests or report issues.
First published