...
 
Commits (14)
......@@ -16,6 +16,7 @@ extends:
plugins:
- vue
- typescript
- pug
rules:
no-console:
......
module.exports = "test-file-stub";
// https://soumak77.github.io/firebase-mock/tutorials/integration/jest.html
import firebasemock from "firebase-mock";
const mockauth = new firebasemock.MockAuthentication();
const mockdatabase = new firebasemock.MockFirebase();
const mockfirestore = new firebasemock.MockFirestore();
const mockstorage = new firebasemock.MockStorage();
const mockmessaging = new firebasemock.MockMessaging();
export const firebase = new firebasemock.MockFirebaseSdk(
// use null if your code does not use RTDB
path => {
return path ? mockdatabase.child(path) : mockdatabase;
},
// use null if your code does not use AUTHENTICATION
() => {
return mockauth;
},
// use null if your code does not use FIRESTORE
() => {
return mockfirestore;
},
// use null if your code does not use STORAGE
() => {
return mockstorage;
},
// use null if your code does not use MESSAGING
() => {
return mockmessaging;
}
);
module.exports = firebase;
import { mount, createLocalVue } from "@vue/test-utils";
import Vuex from "vuex";
const localVue = createLocalVue();
localVue.use(Vuex);
// @ts-ignore
import Component from "../draggableUploadBox.vue";
import * as UploaderStore from "~/store/uploader";
import "~/plugins/buefy";
describe("draggableUploadBox.vue", () => {
let store;
let uploaderStore;
beforeEach(() => {
// generate uploader store
uploaderStore = {
...UploaderStore,
namespaced: true
};
// set action mock
uploaderStore.actions.updateFiles = jest.fn();
uploaderStore.actions.removeFile = jest.fn();
// generate vuex store
store = new Vuex.Store({
modules: { uploader: uploaderStore }
});
});
it("should have `files` value", () => {
const wrapper = mount(Component, { store, localVue });
expect(wrapper.vm).toHaveProperty("files", []);
});
it("dipatches an aciton to update files", () => {
const wrapper = mount(Component, { store, localVue });
const files = [new File([], "test file")];
(wrapper.vm as Component).uploadFiles(files);
expect(uploaderStore.actions.updateFiles).toHaveBeenCalled();
expect(uploaderStore.actions.updateFiles.mock.calls[0][1]).toEqual({
files
});
});
it("dipatches an aciton to delete file", () => {
// 1. add file
const wrapper = mount(Component, { store, localVue });
const files = [new File([], "test file")];
(wrapper.vm as Component).uploadFiles(files);
// 2. remove it
const index = 0;
(wrapper.vm as Component).removeFile(index);
expect(uploaderStore.actions.removeFile).toHaveBeenCalled();
expect(uploaderStore.actions.removeFile.mock.calls[0][1]).toEqual({
index
});
});
});
import { mount, createLocalVue } from "@vue/test-utils";
import Vuex from "vuex";
const localVue = createLocalVue();
localVue.use(Vuex);
// @ts-ignore
import Component from "../tagsInput.vue";
import * as UploaderStore from "~/store/uploader";
import "~/plugins/buefy";
describe("tagsInput.vue", () => {
let store;
let uploaderStore;
beforeEach(() => {
// generate uploader store
uploaderStore = {
...UploaderStore,
namespaced: true
};
// set action mock
uploaderStore.actions.updateTags = jest.fn();
// generate vuex store
store = new Vuex.Store({
modules: { uploader: uploaderStore }
});
});
it("should have `tags` value", () => {
const wrapper = mount(Component, { store, localVue });
expect(wrapper.vm).toHaveProperty("tags", []);
});
it("dipatches an aciton to update tag", () => {
const wrapper = mount(Component, { store, localVue });
const tags = ["a", "b", "c"];
(wrapper.vm as Component).updateTags(tags);
expect(uploaderStore.actions.updateTags).toHaveBeenCalled();
expect(uploaderStore.actions.updateTags.mock.calls[0][1]).toEqual({ tags });
});
});
<template lang="pug">
section
b-field
b-upload(v-model="dropFiles" multiple drag-drop)
section.section
.content.has-text-centered
p
b-icon(icon="upload" size="is-large")
p Drop your files here or click to upload
.tags
span.tag.is-primary(v-for="(file, index) in dropFiles" :key="index") {{ file.name }}
button.delete.is-small(type="button"
@click="deleteDropFile(index)")
div
b-field
b-upload(v-model="files" @input="uploadFiles" multiple drag-drop)
section.section
.content.has-text-centered
p
b-icon(icon="upload" size="is-large")
p ファイルを選ぶと即アップロード!
p (タグは先に書いてね)
.tags
span.tag.is-primary(
v-for="(file, index) in files"
:key="index")
| {{ file.name }}
button.delete.is-small(type="button" @click="removeFile(index)")
</template>
<script lang="ts">
......@@ -18,10 +22,14 @@ import { Vue, Component } from "vue-property-decorator";
@Component
export default class extends Vue {
dropFiles: File[] = [];
files: File[] = [];
async uploadFiles(files: File[]) {
await this.$store.dispatch("uploader/updateFiles", { files });
}
deleteDropFile(index) {
this.dropFiles.splice(index, 1);
async removeFile(index: number) {
await this.$store.dispatch("uploader/removeFile", { index });
}
}
</script>
......@@ -3,7 +3,7 @@
// tag input
tags-input(ref="tagsInput")
// drag & drop space
// drag & drop box
draggable-upload-box(ref="uploadBox")
// upload button
......@@ -13,90 +13,38 @@
<script lang="ts">
import { Vue, Component } from "vue-property-decorator";
import uuid from "uuid/v4"
import DraggableUploadBox from "./draggableUploadBox.vue";
import TagsInput from "./tagsInput.vue";
import * as firebase from "firebase"
@Component({
components: { DraggableUploadBox, TagsInput }
})
export default class extends Vue {
// upload flag for ui control
uploading = false;
get uploading(): boolean {
return this.$store.getters["uploader/uploading"];
}
async upload() {
try {
// check status flag
if (this.uploading) {
this.warning("アップロードちゅう")
return
}
// prepare for uploading
this.uploading = true;
const tagsInput = this.$refs.tagsInput as TagsInput;
const uploadBox = this.$refs.uploadBox as DraggableUploadBox;
// validation
const tags = tagsInput.tags;
const files = uploadBox.dropFiles;
if (!tags.length) {
this.warning("タグが指定されてないよ");
return;
}
if (!files.length) {
this.warning("アップロードするファイルを指定してないよ");
return;
}
// upload files
const uploadResults = files.map(file => this.save(file));
await Promise.all(uploadResults);
// finish uploading
this.info("アップロード完了したよ")
// await this.$store.dispatch('uploader/upload')
this.info("アップロード成功 :)");
} catch (e) {
console.error("アップロード失敗 :(");
console.error(e);
this.warning(`やばいエラーだよ: ${e.message}`);
return;
} finally {
this.uploading = false;
this.error(`${e.message}`);
}
}
async save(file: File): Promise<firebase.storage.UploadTaskSnapshot> {
// generate upload params
const storageRef = this.$firebase.storage().ref();
const fileRef = storageRef.child(`img/${uuid()}/raw`);
const uploadTask = fileRef.put(file);
uploadTask.on(
"state_changed",
// nextOrObserver
() => {},
// error
error => {
alert(error.message);
}
);
return await uploadTask
}
info(message) {
this.$toast.open({
message,
type: "is-danger"
type: "is-info"
});
}
warning(message) {
error(message) {
this.$toast.open({
message,
type: "is-danger"
......
<template lang="pug">
section
b-field(label="Add some tags")
b-taginput( v-model="tags"
ellipsis
icon="label"
placeholder="Add a tag")
b-field(label="Add some tags")
b-taginput(
default
v-model="tags"
ellipsis
icon="label"
@input="updateTags"
placeholder="Add a tag")
</template>
<script lang="ts">
import { mapActions } from "vuex";
import { Vue, Component } from "vue-property-decorator";
@Component
export default class extends Vue {
tags: string[] = [];
tags: string[] = this.$store.getters["uploader/tags"];
async updateTags(tags: string[]) {
await this.$store.dispatch("uploader/updateTags", { tags });
}
}
</script>
......@@ -17,6 +17,7 @@
},
"dependencies": {
"@nuxtjs/pwa": "^2.0.8",
"@types/uuid": "^3.4.4",
"buefy": "^0.6.7",
"firebase": "^5.3.1",
"fp-ts": "^1.7.1",
......@@ -49,9 +50,12 @@
"eslint-config-typescript": "^1.1.0",
"eslint-loader": "^2.1.0",
"eslint-plugin-prettier": "^2.6.2",
"eslint-plugin-pug": "^1.1.1",
"eslint-plugin-typescript": "^0.12.0",
"eslint-plugin-vue": "^5.0.0-0",
"firebase-mock": "^2.2.7",
"fork-ts-checker-webpack-plugin": "^0.4.9",
"inject-loader": "^4.0.1",
"jest": "^23.5.0",
"node-sass": "^4.9.3",
"prettier": "^1.14.2",
......@@ -84,7 +88,7 @@
"transform": {
"^.+\\.js$": "babel-jest",
"^.+\\.(ts|tsx)$": "ts-jest",
".*\\.(vue)$": "vue-jest"
".*\\.vue$": "vue-jest"
},
"globals": {
"ts-jest": {
......@@ -93,6 +97,8 @@
}
},
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|less|scss)$": "<rootDir>/__mocks__/styleMock.js",
"~/(.*)": "<rootDir>/$1"
},
"testMatch": [
......
import Vue from "vue";
// @ts-ignore
import Component from "../index.vue";
describe("index.vue", () => {
it("should render correct contents", () => {
const Constructor = Vue.extend(Component);
const vm = new Constructor().$mount();
expect(vm.$el.textContent).toBe("nuxt: quelplan");
});
});
import { actions, state as defaultState } from "~/store/uploader";
const { removeFile } = actions;
describe("uploader actions: removeFile", () => {
it("commits removal tag index", () => {
const commit = jest.fn();
const index = 10;
removeFile({ commit }, { index });
expect(commit).toBeCalled();
expect(commit.mock.calls[0][0]).toEqual("REMOVE_FILE");
expect(commit.mock.calls[0][1]).toEqual({ index });
});
});
import firebase from "firebase";
import MockStorageReference from "firebase-mock/src/storage-reference";
import { actions } from "~/store/uploader";
const { saveInCloudStorage } = actions;
describe("uploader actions: saveInCloudStorage", () => {
const bindFunction = saveInCloudStorage.bind({ $firebase: firebase });
it("saves image file in cloud storage", async () => {
// call action
const id = "test-id";
const file = new File([], "test file");
await bindFunction({}, { id, file });
// check file
const ref: MockStorageReference = firebase.storage().ref("img/test-id/raw");
expect(ref._contents).not.toBeNull();
});
});
import { actions, state as defaultState } from "~/store/uploader";
const { updateFiles } = actions;
describe("uploader actions: updateTags", () => {
it("commits new tags", () => {
const commit = jest.fn();
const files = [new File([], "test file")];
updateFiles({ commit }, { files });
expect(commit).toBeCalled();
expect(commit.mock.calls[0][0]).toEqual("SAVE_FILES");
expect(commit.mock.calls[0][1]).toEqual({ files });
});
});
import { actions, state as defaultState } from "~/store/uploader";
const { updateTags } = actions;
describe("uploader actions: updateTags", () => {
it("commits new tags", () => {
const commit = jest.fn();
const tags = ["a", "b", "c"];
updateTags({ commit }, { tags });
expect(commit).toBeCalled();
expect(commit.mock.calls[0][0]).toEqual("SAVE_TAGS");
expect(commit.mock.calls[0][1]).toEqual({ tags });
});
});
import { mutations, state as defaultState } from "~/store/uploader";
describe("uploader mutations", () => {
it("SAVE_TAGS", () => {
const state = defaultState();
const tags = ["a", "b", "c"];
mutations["SAVE_TAGS"](state, { tags });
expect(state.tags).toEqual(tags);
});
it("SAVE_FILES", () => {
const state = defaultState();
const files = [new File([], "test file")];
mutations["SAVE_FILES"](state, { files });
expect(state.files).toEqual(files);
});
it("REMOVE_FILE", () => {
// 1. add file
const state = defaultState();
const file1 = new File([], "test file 1");
const file2 = new File([], "test file 2");
const file3 = new File([], "test file 3");
const files = [file1, file2, file3];
mutations["SAVE_FILES"](state, { files });
expect(state.files).toEqual(files);
// 2. remove it
mutations["REMOVE_FILE"](state, { index: 1 });
expect(state.files).toEqual([file1, file3]);
});
it("UPLOADING", () => {
const state = defaultState();
// on
mutations["UPLOADING"](state, { uploading: true });
expect(state.uploading).toEqual(true);
// off
mutations["UPLOADING"](state, { uploading: false });
expect(state.uploading).toEqual(false);
});
});
export interface IState {
auth: IAuthState;
uploader: IUploaderState;
}
export interface IAuthState {
initialized: boolean;
loggedIn: boolean;
}
export interface IUploaderState {
uploading: boolean;
tags: string[];
files: File[];
}
import firebase from "firebase";
import Vue from "vue";
import { GetterTree, ActionTree, MutationTree } from "vuex";
import uuid from "uuid/v4";
import { IState, IUploaderState } from "./index";
type ModuleState = IUploaderState;
/*
* state
*/
export const state = (): ModuleState => ({
tags: [],
files: [],
uploading: false
});
/*
* getters
*/
export const getters: GetterTree<ModuleState, IState> = {
uploading(state): boolean {
return !!state.uploading;
},
tags(state): string[] {
return Object.assign([], state.tags);
},
files(state): File[] {
return Object.assign([], state.files);
}
};
/*
* actions
*/
export interface IActions extends ActionTree<ModuleState, IState> {
updateTags({ commit }, props: { tags: string[] });
updateFiles({ commit }, props: { files: File[] });
removeFile({ commit }, props: { index: number });
upload({ state, commit, dispatch }): Promise<void>;
saveInCloudStorage(
_,
props: { id: string; file: File }
): Promise<firebase.storage.UploadTaskSnapshot>;
}
export const actions: IActions = {
updateTags({ commit }, props) {
commit("SAVE_TAGS", props);
},
updateFiles({ commit }, props) {
commit("SAVE_FILES", props);
},
removeFile({ commit }, props: { index: number }) {
commit("REMOVE_FILE", props);
},
async upload({ state, commit, dispatch }) {
// check status flag
if (state.uploading) {
throw new Error("アップロード中");
}
// validation
if (!state.tags.length) {
throw new Error("タグが指定されてないよ");
}
if (!state.files.length) {
throw new Error("アップロードするファイルを指定してないよ");
}
// prepare for uploading
commit("UPLOADING", { uploading: true });
// upload files
const uploadResults = state.files.map(file => {
const id = uuid();
return dispatch("saveInCloudStorage", { id, file });
});
await Promise.all(uploadResults);
// finish
commit("UPLOADING", { uploading: false });
},
async saveInCloudStorage(
_,
{ id, file }
): Promise<firebase.storage.UploadTaskSnapshot> {
// generate upload params
const fileRef = this.$firebase.storage().ref(`img/${id}/raw`);
const uploadTask = fileRef.put(file);
return await uploadTask;
}
};
/*
* mutations
*/
export const mutations: MutationTree<ModuleState> = {
SAVE_TAGS(state, props: { tags: string[] }) {
const tags = Object.assign([], props.tags);
Vue.set(state, "tags", tags);
},
SAVE_FILES(state, props: { files: File[] }) {
const files = Object.assign([], props.files);
Vue.set(state, "files", files);
},
REMOVE_FILE(state, props: { index: number }) {
state.files.splice(props.index, 1);
},
UPLOADING(state, props: { uploading: boolean }) {
state.uploading = props.uploading;
}
};
This diff is collapsed.