From e52952824d0a92b72c1a6f9649f428d1ae9f0936 Mon Sep 17 00:00:00 2001 From: The Jared Wilcurt Date: Sun, 10 May 2020 08:59:01 -0400 Subject: [PATCH 1/5] Build and test for web and desktop --- README.md | 39 ++++++ src/App.vue | 11 +- src/components/FsExample.vue | 19 +-- src/components/HelloWorld.vue | 22 +-- src/components/LinkList.vue | 6 +- src/helpers/applyPrototypes.js | 11 ++ src/main.js | 5 +- tests/unit/App.test.js | 24 +++- tests/unit/__snapshots__/App.test.js.snap | 10 +- tests/unit/components/FsExample.test.js | 89 +++++++++---- tests/unit/components/HelloWorld.test.js | 83 ++++++++---- tests/unit/components/LinkList.test.js | 108 +++++++++++---- .../__snapshots__/FsExample.test.js.snap | 8 +- .../__snapshots__/HelloWorld.test.js.snap | 126 +++++++++++++++++- .../__snapshots__/LinkList.test.js.snap | 12 +- tests/unit/setup.js | 39 ++++-- 16 files changed, 491 insertions(+), 121 deletions(-) create mode 100644 src/helpers/applyPrototypes.js diff --git a/README.md b/README.md index bdab0b6..4f623e4 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,12 @@ NW.js + Vue-CLI 4 example ![A screenshot of the default app running on Windows](screenshot.png) +*Does this work for web or just desktop?* + +This repo will build both for web and desktop and includes simple boolean logic so you can add Desktop unique features behind the `isDesktop` flag. These repo has 100% test coverage including tests for both web and desktop builds. You could even theoretically add NativeScript-vue into the mix and build for native mobile as well (though that is not set up in this repo). + +**Comes with:** + * NW.js 0.44.2 * Chrome 80 * Node 13.8.0 @@ -25,6 +31,39 @@ NW.js + Vue-CLI 4 example Those are both very easily added from the Vue-CLI. There is also no custom styling libraries (Bulma, Bootstrap, etc), or meta-languages (Sass, TS, Pug, etc), or component libraries (Vuetify, Inkline, etc). This repo is meant to be the "go to" option for building all desktop apps with Vue. So it avoids pushing any particular choices on to you. With the exception of testing being set up for Jest, and Linting being set up to ensure minumum quality of this boilerplate repo itself. Both of which can be easily modified, ignored, or removed. +## Documentation + +In all .vue components, you have access to `nw`, `global`, `process`, `require`, and the boolean `isDesktop`: + +```js +methods: { + example: function () { + if (this.isDesktop) { + console.log('Your OS is ' + this.process.platform); + console.log('Your AppData location is ' + this.nw.App.dataPath); + // Sets a value on Node's global, meaning other windows have access to this data. + this.global.cow = 'moo' + // The contents of the current directory + console.log(this.require('fs').readdirSync('.')); + } + } +} +``` + +Or even directly from the template (with some slight changes to work within the Vue context): +```html +
+ Your OS is {{ process.platform }}. + Your AppData location is {{ nw.App.dataPath }}. + + The contents of the current directory are {{ nw.require('fs').readdirSync('.') }}. +
+``` + + ## Running Locally for development 1. `npm install` diff --git a/src/App.vue b/src/App.vue index b0564c2..c66fb5a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -7,12 +7,13 @@ class="logo" /> - + @@ -23,6 +24,14 @@ export default { name: 'App', components: { HelloWorld + }, + computed: { + message: function () { + if (this.isDesktop) { + return 'Welcome to your Vue.js Desktop App in NW.js!'; + } + return 'Welcome to your Vue.js Web App!'; + } } }; diff --git a/src/components/FsExample.vue b/src/components/FsExample.vue index ee6bb5d..733f9ae 100644 --- a/src/components/FsExample.vue +++ b/src/components/FsExample.vue @@ -1,5 +1,5 @@ @@ -154,19 +160,19 @@ export default { methods: { toggleDevTools: function () { if (this.showDevTools) { - window.nw.Window.get().showDevTools(); + this.nw.Window.get().showDevTools(); } else { - window.nw.Window.get().closeDevTools(); + this.nw.Window.get().closeDevTools(); } this.showDevTools = !this.showDevTools; } }, computed: { devMode: function () { - return window.process.versions['nw-flavor'] === 'sdk'; + return this.isDesktop && this.process.versions['nw-flavor'] === 'sdk'; }, versions: function () { - return window.process.versions; + return this.isDesktop && this.process.versions; }, vueVersion: function () { return Vue.version; diff --git a/src/components/LinkList.vue b/src/components/LinkList.vue index c1ec52d..d34333a 100644 --- a/src/components/LinkList.vue +++ b/src/components/LinkList.vue @@ -44,7 +44,11 @@ export default { }, methods: { open: function (url) { - window.nw.Shell.openExternal(url); + if (this.isDesktop) { + nw.Shell.openExternal(url); + } else { + window.open(url, '_blank'); + } } } }; diff --git a/src/helpers/applyPrototypes.js b/src/helpers/applyPrototypes.js new file mode 100644 index 0000000..a0d175c --- /dev/null +++ b/src/helpers/applyPrototypes.js @@ -0,0 +1,11 @@ +// Make NW.js and Node globals available in Vue +export default function applyPrototypes (Vue) { + Vue.prototype.isDesktop = !!window.nw; + + if (window.nw) { + Vue.prototype.nw = window.nw; + Vue.prototype.process = window.nw.process; + Vue.prototype.require = window.nw.require; + Vue.prototype.global = global; + } +} diff --git a/src/main.js b/src/main.js index 2426431..2dcb210 100644 --- a/src/main.js +++ b/src/main.js @@ -1,7 +1,10 @@ import Vue from 'vue'; -import App from './App.vue'; + +import App from '@/App.vue'; +import applyPrototypes from '@/helpers/applyPrototypes.js'; Vue.config.productionTip = false; +applyPrototypes(Vue); // eslint-disable-next-line no-unused-vars const app = new Vue({ diff --git a/tests/unit/App.test.js b/tests/unit/App.test.js index eeb36ea..90c1d6b 100644 --- a/tests/unit/App.test.js +++ b/tests/unit/App.test.js @@ -1,11 +1,27 @@ import { shallowMount } from '@vue/test-utils'; + import App from '@/App.vue'; describe('App.vue', () => { - test('Render default contents', () => { - const wrapper = shallowMount(App); + describe('Desktop', () => { + test('Render default contents', () => { + const wrapper = shallowMount(App); + + expect(wrapper) + .toMatchSnapshot(); + }); + }); + + describe('Web', () => { + beforeEach(() => { + window.webSetup(); + }); + + test('Render default contents', () => { + const wrapper = shallowMount(App); - expect(wrapper) - .toMatchSnapshot(); + expect(wrapper) + .toMatchSnapshot(); + }); }); }); diff --git a/tests/unit/__snapshots__/App.test.js.snap b/tests/unit/__snapshots__/App.test.js.snap index 4e95728..5fc06f5 100644 --- a/tests/unit/__snapshots__/App.test.js.snap +++ b/tests/unit/__snapshots__/App.test.js.snap @@ -1,9 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`App.vue Render default contents 1`] = ` +exports[`App.vue Desktop Render default contents 1`] = `
`; + +exports[`App.vue Web Render default contents 1`] = ` +
+ + + +
+`; diff --git a/tests/unit/components/FsExample.test.js b/tests/unit/components/FsExample.test.js index 2183f6f..1cf4a04 100644 --- a/tests/unit/components/FsExample.test.js +++ b/tests/unit/components/FsExample.test.js @@ -1,45 +1,80 @@ import { shallowMount } from '@vue/test-utils'; + import FsExample from '@/components/FsExample.vue'; describe('FsExample.vue', () => { - test('Render default contents', () => { - const wrapper = shallowMount(FsExample); + const shared = { + renderDefaultContents: function () { + const wrapper = shallowMount(FsExample); + return wrapper; + } + }; - expect(wrapper) - .toMatchSnapshot(); - }); + describe('Desktop', () => { + test('Render default contents', () => { + const wrapper = shared.renderDefaultContents(); + + expect(wrapper) + .toMatchSnapshot(); + }); - test('Click button', async () => { - const wrapper = shallowMount(FsExample); - let domButton = wrapper.find('[data-test="fs-example-button"]'); - domButton.trigger('click'); + test('Click button', async () => { + const wrapper = shallowMount(FsExample); + let domButton = wrapper.find('[data-test="fs-example-button"]'); + domButton.trigger('click'); + + await wrapper.vm.$nextTick(); + + expect(window.nw.require) + .toHaveBeenCalledWith('fs'); + + expect(wrapper) + .toMatchSnapshot(); + }); - await wrapper.vm.$nextTick(); + test('Error state', async () => { + window.nw.require.mockImplementation((module) => { + if (module === 'fs') { + return new Error(); + } + }); - expect(window.nw.require) - .toHaveBeenCalledWith('fs'); + const wrapper = shallowMount(FsExample); + let domButton = wrapper.find('[data-test="fs-example-button"]'); + domButton.trigger('click'); - expect(wrapper) - .toMatchSnapshot(); + await wrapper.vm.$nextTick(); + + expect(window.nw.require) + .toHaveBeenCalledWith('fs'); + + expect(wrapper) + .toMatchSnapshot(); + }); }); - test('Error state', async () => { - window.nw.require.mockImplementation((module) => { - if (module === 'fs') { - return new Error(); - } + describe('Web', () => { + beforeEach(() => { + window.webSetup(); }); - const wrapper = shallowMount(FsExample); - let domButton = wrapper.find('[data-test="fs-example-button"]'); - domButton.trigger('click'); + test('Render default contents', () => { + const wrapper = shared.renderDefaultContents(); - await wrapper.vm.$nextTick(); + expect(wrapper) + .toMatchSnapshot(); + }); - expect(window.nw.require) - .toHaveBeenCalledWith('fs'); + test('getCurrentDirectory', () => { + const wrapper = shallowMount(FsExample); - expect(wrapper) - .toMatchSnapshot(); + wrapper.vm.getCurrentDirectory(); + + expect(wrapper.vm.contents) + .toEqual(null); + + expect(wrapper.vm.error) + .toEqual(false); + }); }); }); diff --git a/tests/unit/components/HelloWorld.test.js b/tests/unit/components/HelloWorld.test.js index 3dafd23..52cf325 100644 --- a/tests/unit/components/HelloWorld.test.js +++ b/tests/unit/components/HelloWorld.test.js @@ -1,39 +1,76 @@ import { shallowMount, mount } from '@vue/test-utils'; + import HelloWorld from '@/components/HelloWorld.vue'; describe('HelloWorld.vue', () => { - test('Render props.msg', () => { - const msg = 'new message'; - const wrapper = shallowMount(HelloWorld, { - propsData: { msg } + const shared = { + msg: 'new message', + renderPropsMsg: function () { + const wrapper = shallowMount(HelloWorld, { + propsData: { + msg: this.msg + } + }); + + return wrapper; + }, + renderDefaultContents: function () { + const wrapper = mount(HelloWorld); + return wrapper; + } + }; + + describe('Desktop', () => { + test('Render props.msg', () => { + const wrapper = shared.renderPropsMsg(); + + expect(wrapper.find('[data-test="message"]').text()) + .toEqual(shared.msg); }); - expect(wrapper.find('[data-test="message"]').text()) - .toEqual(msg); - }); + test('Render default contents', () => { + const wrapper = shared.renderDefaultContents(); + + expect(wrapper) + .toMatchSnapshot(); + }); - test('Render default contents', () => { - const wrapper = mount(HelloWorld); + test('Activate dev tools', async () => { + const wrapper = shallowMount(HelloWorld); - expect(wrapper) - .toMatchSnapshot(); - }); + const button = wrapper.find('[data-test="toggleDevTools"]'); - test('Activate dev tools', async () => { - const wrapper = shallowMount(HelloWorld); + button.trigger('click'); + await wrapper.vm.$nextTick(); - const button = wrapper.find('[data-test="toggleDevTools"]'); + expect(wrapper.find('[data-test="toggleDevTools').html()) + .toMatchSnapshot('hide'); - button.trigger('click'); - await wrapper.vm.$nextTick(); + button.trigger('click'); + await wrapper.vm.$nextTick(); - expect(wrapper.find('[data-test="toggleDevTools').html()) - .toMatchSnapshot('hide'); + expect(wrapper.find('[data-test="toggleDevTools').html()) + .toMatchSnapshot('show'); + }); + }); + + describe('Web', () => { + beforeEach(() => { + window.webSetup(); + }); - button.trigger('click'); - await wrapper.vm.$nextTick(); + test('Render props.msg', () => { + const wrapper = shared.renderPropsMsg(); - expect(wrapper.find('[data-test="toggleDevTools').html()) - .toMatchSnapshot('show'); + expect(wrapper.find('[data-test="message"]').text()) + .toEqual(shared.msg); + }); + + test('Render default contents', () => { + const wrapper = shared.renderDefaultContents(); + + expect(wrapper) + .toMatchSnapshot(); + }); }); }); diff --git a/tests/unit/components/LinkList.test.js b/tests/unit/components/LinkList.test.js index fcc754b..958d5b1 100644 --- a/tests/unit/components/LinkList.test.js +++ b/tests/unit/components/LinkList.test.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; + import LinkList from '@/components/LinkList.vue'; describe('LinkList.vue', () => { @@ -7,44 +8,99 @@ describe('LinkList.vue', () => { url: 'https://nwjs.io' }; - test('Validate props', () => { - const wrapper = shallowMount(LinkList); - const links = wrapper.vm.$options.props.links; + const shared = { + validateProps: function () { + const wrapper = shallowMount(LinkList); + const links = wrapper.vm.$options.props.links; + return links; + }, + renderDefaultContents: function () { + const wrapper = shallowMount(LinkList, { + propsData: { links: [link] } + }); + return wrapper; + }, + clickLink: function () { + const wrapper = shallowMount(LinkList, { + propsData: { links: [link] } + }); - expect(links.required) - .toBeFalsy(); + let domLink = wrapper.findAll('[data-test="link"]').at(0); + domLink.trigger('click'); + } + }; - expect(links.type) - .toBe(Array); + describe('Desktop', () => { + test('Validate props', () => { + const links = shared.validateProps(expect); - expect(links.default) - .toBeNull(); + expect(links.required) + .toBeFalsy(); - expect(links.validator && links.validator([{ name: '', url: '' }])) - .toBeFalsy(); + expect(links.type) + .toBe(Array); - expect(links.validator && links.validator([link])) - .toBeTruthy(); - }); + expect(links.default) + .toBeNull(); + + expect(links.validator && links.validator([{ name: '', url: '' }])) + .toBeFalsy(); + + expect(links.validator && links.validator([link])) + .toBeTruthy(); + }); + + test('Render default contents', () => { + const wrapper = shared.renderDefaultContents(); - test('Render default contents', () => { - const wrapper = shallowMount(LinkList, { - propsData: { links: [link] } + expect(wrapper) + .toMatchSnapshot(); }); - expect(wrapper) - .toMatchSnapshot(); + test('Click link', () => { + shared.clickLink(); + + expect(window.nw.Shell.openExternal) + .toHaveBeenCalledWith('https://nwjs.io'); + }); }); - test('Click link', () => { - const wrapper = shallowMount(LinkList, { - propsData: { links: [link] } + describe('Web', () => { + beforeEach(() => { + window.webSetup(); + }); + + test('Validate props', () => { + const links = shared.validateProps(); + + expect(links.required) + .toBeFalsy(); + + expect(links.type) + .toBe(Array); + + expect(links.default) + .toBeNull(); + + expect(links.validator && links.validator([{ name: '', url: '' }])) + .toBeFalsy(); + + expect(links.validator && links.validator([link])) + .toBeTruthy(); }); - let domLink = wrapper.findAll('[data-test="link"]').at(0); - domLink.trigger('click'); + test('Render default contents', () => { + const wrapper = shared.renderDefaultContents(); + + expect(wrapper) + .toMatchSnapshot(); + }); - expect(window.nw.Shell.openExternal) - .toHaveBeenCalledWith('https://nwjs.io'); + test('Click link', () => { + shared.clickLink(); + + expect(window.open) + .toHaveBeenCalledWith('https://nwjs.io', '_blank'); + }); }); }); diff --git a/tests/unit/components/__snapshots__/FsExample.test.js.snap b/tests/unit/components/__snapshots__/FsExample.test.js.snap index 25effa6..6046f23 100644 --- a/tests/unit/components/__snapshots__/FsExample.test.js.snap +++ b/tests/unit/components/__snapshots__/FsExample.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FsExample.vue Click button 1`] = ` +exports[`FsExample.vue Desktop Click button 1`] = `
`; -exports[`FsExample.vue Error state 1`] = ` +exports[`FsExample.vue Desktop Error state 1`] = `
There was an error attempting to read from the file system. @@ -32,7 +32,7 @@ exports[`FsExample.vue Error state 1`] = `
`; -exports[`FsExample.vue Render default contents 1`] = ` +exports[`FsExample.vue Desktop Render default contents 1`] = `
`; + +exports[`FsExample.vue Web Render default contents 1`] = ``; diff --git a/tests/unit/components/__snapshots__/HelloWorld.test.js.snap b/tests/unit/components/__snapshots__/HelloWorld.test.js.snap index 1f8f0cc..ccf5428 100644 --- a/tests/unit/components/__snapshots__/HelloWorld.test.js.snap +++ b/tests/unit/components/__snapshots__/HelloWorld.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`HelloWorld.vue Activate dev tools: hide 1`] = ` +exports[`HelloWorld.vue Desktop Activate dev tools: hide 1`] = ` `; -exports[`HelloWorld.vue Activate dev tools: show 1`] = ` +exports[`HelloWorld.vue Desktop Activate dev tools: show 1`] = ` `; -exports[`HelloWorld.vue Render default contents 1`] = ` +exports[`HelloWorld.vue Desktop Render default contents 1`] = `

Welcome to your Vue Desktop App in NW.js! @@ -151,3 +151,123 @@ exports[`HelloWorld.vue Render default contents 1`] = `

`; + +exports[`HelloWorld.vue Web Render default contents 1`] = ` +
+

+ Welcome to your Vue Desktop App in NW.js! +

+

+ You are using Vue.js (v2.6.11). +

+ +

You can use the resources below to find more information around building your Vue App.

+ +

Installed CLI Plugins

+ +

Essential Links

+ +

Ecosystem

+ + +
+`; diff --git a/tests/unit/components/__snapshots__/LinkList.test.js.snap b/tests/unit/components/__snapshots__/LinkList.test.js.snap index 86472df..d4f0d24 100644 --- a/tests/unit/components/__snapshots__/LinkList.test.js.snap +++ b/tests/unit/components/__snapshots__/LinkList.test.js.snap @@ -1,6 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`LinkList.vue Render default contents 1`] = ` +exports[`LinkList.vue Desktop Render default contents 1`] = ` + +`; + +exports[`LinkList.vue Web Render default contents 1`] = `
  • diff --git a/tests/unit/setup.js b/tests/unit/setup.js index 563eabb..4013fc4 100644 --- a/tests/unit/setup.js +++ b/tests/unit/setup.js @@ -1,5 +1,7 @@ import Vue from 'vue'; +import applyPrototypes from '@/helpers/applyPrototypes.js'; + const { getComputedStyle } = window; Vue.config.productionTip = false; @@ -20,20 +22,27 @@ window.getComputedStyle = function getComputedStyleStub (el) { }; }; +window.webSetup = function () { + delete window.nw; + applyPrototypes(Vue); + + window.open = jest.fn(); +}; + global.beforeEach(() => { - window.process = { - cwd: process.cwd, - env: { - NODE_ENV: 'development' - }, - versions: { - chromium: '80.0.3987.116', - nw: '0.44.2', - 'nw-flavor': 'sdk', - node: '13.8.0' - } - }; window.nw = { + process: { + cwd: process.cwd, + env: { + NODE_ENV: 'development' + }, + versions: { + chromium: '80.0.3987.116', + nw: '0.44.2', + 'nw-flavor': 'sdk', + node: '13.8.0' + } + }, require: jest.fn((module) => { if (module === 'fs') { return { @@ -55,10 +64,14 @@ global.beforeEach(() => { } } }; + + applyPrototypes(Vue); }); global.afterEach(() => { - window.nw.Window.get().showDevTools.mockClear(); + if (window.nw) { + window.nw.Window.get().showDevTools.mockClear(); + } }); From d74000a3cacb1d2601395b2adf22a3c2ab2bac02 Mon Sep 17 00:00:00 2001 From: The Jared Wilcurt <4629794+TheJaredWilcurt@users.noreply.github.com> Date: Sun, 13 Jun 2021 09:34:17 -0400 Subject: [PATCH 2/5] Add link --- README.md | 2 +- src/components/HelloWorld.vue | 9 +++++++++ src/components/LinkList.vue | 11 +++-------- .../components/__snapshots__/HelloWorld.test.js.snap | 10 ++++++++++ 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4f623e4..d77cd19 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ NW.js + Vue-CLI 4 example *Does this work for web or just desktop?* -This repo will build both for web and desktop and includes simple boolean logic so you can add Desktop unique features behind the `isDesktop` flag. These repo has 100% test coverage including tests for both web and desktop builds. You could even theoretically add NativeScript-vue into the mix and build for native mobile as well (though that is not set up in this repo). +This repo will build both for web and desktop and includes a simple `this.isDesktop` flag so you can add Desktop specific features that won't show on the web. This repo has 100% test coverage including tests for both web and desktop builds. You could even theoretically add NativeScript-vue into the mix and build for native mobile as well (though that is not set up in this repo). **Comes with:** diff --git a/src/components/HelloWorld.vue b/src/components/HelloWorld.vue index 62de1ca..66bde42 100644 --- a/src/components/HelloWorld.vue +++ b/src/components/HelloWorld.vue @@ -16,6 +16,15 @@ You are using Vue.js (v{{ vueVersion }}). +
    + +
    +