@@ -19,7 +19,7 @@ describe("Password Generation", function() { | |||
} | |||
cy.visit("/"); | |||
cy.get("#siteField").type("lesspass.com").blur(); | |||
cy.get("#siteField").type("lesspass.com"); | |||
cy.get("#login").type("test@lesspass.com"); | |||
cy.get("#passwordField").type("test@lesspass.com"); | |||
cy.wait(500); | |||
@@ -105,16 +105,31 @@ describe("Password Generation", function() { | |||
}); | |||
it("should generate password when hit enter nrt_266", function() { | |||
cy.visit("/"); | |||
cy.get("#siteField").type("lesspass.com").blur(); | |||
cy.get("#siteField").type("lesspass.com"); | |||
cy.get("#login").type("test@lesspass.com"); | |||
cy.get("#passwordField") | |||
.type("test@lesspass.com") | |||
.type("{enter}"); | |||
cy.get("#generated-password").should("have.value", "hjV@\\5ULp3bIs,6B"); | |||
}); | |||
it("should keep site field in sync nrt_441", function() { | |||
cy.visit("/"); | |||
cy.get("#login").type("user"); | |||
cy.get("#passwordField").type("password"); | |||
cy.get("#siteField") | |||
.type("subdomain.domain.com") | |||
.type("{home}") | |||
.type("{rightarrow}") | |||
.type("{backspace}") | |||
.type("{downarrow}") | |||
.type("{downarrow}") | |||
.type("{enter}"); | |||
cy.get("#generatePassword__btn").click(); | |||
cy.get("#generated-password").should("have.value", "ZT^IK2e!t@k$9`*)"); | |||
}); | |||
it("should clear password generated when master password change", function() { | |||
cy.visit("/"); | |||
cy.get("#siteField").type("example.org").blur(); | |||
cy.get("#siteField").type("example.org"); | |||
cy.get("#login").type("user"); | |||
cy.get("#passwordField").type("password"); | |||
cy.get("#generatePassword__btn").should("be.visible"); | |||
@@ -1,6 +1,9 @@ | |||
import { mount } from "@vue/test-utils"; | |||
import InputSite from "./InputSite.vue"; | |||
jest.mock("../services/url-parser"); | |||
import { getSuggestions } from "../services/url-parser"; | |||
const createWrapper = data => | |||
mount({ | |||
data: () => { | |||
@@ -19,6 +22,10 @@ const optionsFor = wrapper => wrapper.findAll("div.awesomplete li"); | |||
const inputField = wrapper => wrapper.find("#siteField"); | |||
describe("InputSite", () => { | |||
beforeEach(() => { | |||
getSuggestions.mockImplementation(() => []); | |||
}); | |||
it("fills the input with the value property", () => { | |||
const wrapper = createWrapper({ site: "lesspass.com" }); | |||
const input = inputField(wrapper); | |||
@@ -30,8 +37,8 @@ describe("InputSite", () => { | |||
it("filters according to site name", () => { | |||
const wrapper = createWrapper({ | |||
passwords: [ | |||
{ id: "p1", site: "lesspass", login: "xavier" }, | |||
{ id: "p2", site: "wrongsite", login: "xavier" } | |||
{ site: "lesspass", login: "xavier" }, | |||
{ site: "wrongsite", login: "xavier" } | |||
] | |||
}); | |||
inputField(wrapper).setValue("le"); | |||
@@ -41,16 +48,25 @@ describe("InputSite", () => { | |||
}); | |||
it(`shows options that are contained in the user's value`, () => { | |||
const wrapper = createWrapper({ | |||
passwords: [{ id: "p3", site: "lesspass", login: "xavier" }] | |||
passwords: [{ site: "lesspass", login: "xavier" }] | |||
}); | |||
inputField(wrapper).setValue("www.lesspass.com"); | |||
let options = optionsFor(wrapper); | |||
expect(options.length).toBe(1); | |||
expect(options.at(0).text()).toBe("lesspass xavier"); | |||
}); | |||
it("filters using suggestion", () => { | |||
getSuggestions.mockImplementation(() => []); | |||
const wrapper = createWrapper(); | |||
getSuggestions.mockImplementation(() => ["lesspass"]); | |||
inputField(wrapper).setValue("lesspass.com"); | |||
let options = optionsFor(wrapper); | |||
expect(options.length).toBe(1); | |||
expect(options.at(0).text()).toBe("lesspass"); | |||
}); | |||
it("shows site and login in the list", () => { | |||
const wrapper = createWrapper({ | |||
passwords: [{ id: "p4", site: "lesspass", login: "xavier" }] | |||
passwords: [{ site: "lesspass", login: "xavier" }] | |||
}); | |||
inputField(wrapper).setValue("le"); | |||
let options = optionsFor(wrapper); | |||
@@ -59,7 +75,7 @@ describe("InputSite", () => { | |||
}); | |||
it(`doesn't use login`, () => { | |||
const wrapper = createWrapper({ | |||
passwords: [{ id: "p5", site: "lesspass", login: "xavier" }] | |||
passwords: [{ site: "lesspass", login: "xavier" }] | |||
}); | |||
inputField(wrapper).setValue("xa"); | |||
let options = optionsFor(wrapper); | |||
@@ -68,10 +84,10 @@ describe("InputSite", () => { | |||
it(`prints options sorted by site then login`, () => { | |||
const wrapper = createWrapper({ | |||
passwords: [ | |||
{ id: "p6", site: "lesspass", login: "guillaume" }, | |||
{ id: "p7", site: "passless", login: "xavier" }, | |||
{ id: "p8", site: "passless", login: "guillaume" }, | |||
{ id: "p9", site: "lesspass", login: "xavier" } | |||
{ site: "lesspass", login: "guillaume" }, | |||
{ site: "passless", login: "xavier" }, | |||
{ site: "passless", login: "guillaume" }, | |||
{ site: "lesspass", login: "xavier" } | |||
] | |||
}); | |||
inputField(wrapper).setValue("le"); | |||
@@ -88,7 +104,7 @@ describe("InputSite", () => { | |||
let wrapper; | |||
beforeEach(() => { | |||
wrapper = createWrapper({ | |||
passwords: [{ id: "p10", site: "lesspass", login: "xavier" }] | |||
passwords: [{ site: "lesspass", login: "xavier" }] | |||
}); | |||
inputField(wrapper).setValue("le"); | |||
const options = optionsFor(wrapper); | |||
@@ -99,13 +115,32 @@ describe("InputSite", () => { | |||
}); | |||
it('emits a "passwordProfileSelected" with the value', () => { | |||
const emitted = wrapper.find(InputSite).emitted(); | |||
const events = emitted["passwordProfileSelected"]; | |||
expect(events.length).toBe(1); | |||
expect(events[0]).toEqual([ | |||
{ id: "p10", site: "lesspass", login: "xavier" } | |||
const profileSelected = emitted["passwordProfileSelected"]; | |||
expect(profileSelected.length).toBe(1); | |||
expect(profileSelected[0]).toEqual([ | |||
{ site: "lesspass", login: "xavier" } | |||
]); | |||
}); | |||
}); | |||
}); | |||
describe("when selecting suggestion", () => { | |||
let wrapper; | |||
beforeEach(() => { | |||
getSuggestions.mockImplementation(() => ["lesspass"]); | |||
wrapper = createWrapper(); | |||
inputField(wrapper).setValue("le"); | |||
const options = optionsFor(wrapper); | |||
options.at(0).trigger("click"); | |||
}); | |||
it("completes field", () => { | |||
expect(inputField(wrapper).element.value).toBe("lesspass"); | |||
}); | |||
it('emits a "suggestionSelected" with site value', () => { | |||
const emitted = wrapper.find(InputSite).emitted(); | |||
const profileSelected = emitted["suggestionSelected"]; | |||
expect(profileSelected.length).toBe(1); | |||
expect(profileSelected[0]).toEqual(["lesspass"]); | |||
}); | |||
}); | |||
}); | |||
}); |
@@ -39,12 +39,7 @@ | |||
} | |||
}, | |||
mounted() { | |||
const siteField = this.$refs.siteField; | |||
this.awesomplete = new Awesomplete(siteField,{ | |||
minChars: 0, | |||
maxItems: 5 | |||
}); | |||
this.awesomplete.list = this.passwords; | |||
this.awesomplete = new Awesomplete(this.$refs.siteField); | |||
this.awesomplete.item = (element, input) => { | |||
let item = Awesomplete.ITEM(element.value.site, input); | |||
item.innerHTML += ` ${element.value.login}`; | |||
@@ -57,10 +52,13 @@ | |||
this.awesomplete.data = data => { | |||
return {label: data.site, value: data} | |||
}; | |||
this.awesomplete.replace = suggestion => { | |||
siteField.value = suggestion.label; | |||
const passwordProfile = suggestion.value; | |||
this.$emit("passwordProfileSelected", suggestion.value); | |||
this.awesomplete.replace = password => { | |||
this.$refs.siteField.value = password.label; | |||
if (password.value.suggestion) { | |||
this.$emit("suggestionSelected", password.value.site); | |||
} else { | |||
this.$emit("passwordProfileSelected", password.value); | |||
} | |||
}; | |||
this.awesomplete.sort = (a,b) => { | |||
return a.value.site.localeCompare(b.value.site) || | |||
@@ -75,14 +73,14 @@ | |||
set: function (newValue) { | |||
this.$emit("input", newValue); | |||
} | |||
}, | |||
suggestions: function () { | |||
return this.passwords | |||
} | |||
}, | |||
watch: { | |||
suggestions: function (newValue, _) { | |||
this.awesomplete.list = newValue; | |||
site: function (newValue, _) { | |||
const suggestions = getSuggestions(newValue).map(suggestion => { | |||
return {site: suggestion, suggestion: true, login: ''} | |||
}); | |||
this.awesomplete.list = this.passwords.concat(suggestions); | |||
} | |||
}, | |||
methods: {} | |||
@@ -17,8 +17,8 @@ export const saveDefaultOptions = ({ commit }, payload) => { | |||
commit(types.SET_DEFAULT_OPTIONS, payload); | |||
}; | |||
export const addSuggestions = ({ commit }, { site }) => { | |||
commit(types.ADD_SUGGESTIONS, { site }); | |||
export const loadPasswordProfile = ({ commit }, { site }) => { | |||
commit(types.LOAD_PASSWORD_PROFILE, { site }); | |||
}; | |||
export const getPasswordFromUrlQuery = ({ commit }, { query }) => { | |||
@@ -8,6 +8,6 @@ export const SET_PASSWORDS = "SET_PASSWORDS"; | |||
export const SET_TOKEN = "SET_TOKEN"; | |||
export const RESET_PASSWORD = "RESET_PASSWORD"; | |||
export const SET_SITE = "SET_SITE"; | |||
export const ADD_SUGGESTIONS = "ADD_SUGGESTIONS"; | |||
export const LOAD_PASSWORD_PROFILE = "LOAD_PASSWORD_PROFILE"; | |||
export const DELETE_PASSWORD = "DELETE_PASSWORD"; | |||
export const CLEAN_MESSAGE = "CLEAN_MESSAGE"; |
@@ -1,5 +1,4 @@ | |||
import * as types from "./mutation-types"; | |||
import { getSuggestions } from "../services/url-parser"; | |||
export default { | |||
[types.LOGIN](state) { | |||
@@ -39,19 +38,22 @@ export default { | |||
[types.SET_SITE](state, { site }) { | |||
state.password.site = site; | |||
}, | |||
[types.ADD_SUGGESTIONS](state, { site }) { | |||
if (!site) return; | |||
[types.LOAD_PASSWORD_PROFILE](state, { site }) { | |||
if (!site || typeof state.password.id !== "undefined") { | |||
return; | |||
} | |||
state.password = Object.assign({}, state.password, { site }); | |||
const passwords = state.passwords || []; | |||
const passwordsSites = passwords.map(p => p.site); | |||
const suggestions = getSuggestions(site) | |||
.filter(suggestion => passwordsSites.indexOf(suggestion) !== 1) | |||
.map(suggestion => { | |||
return { | |||
...state.defaultPassword, | |||
site: suggestion | |||
}; | |||
}); | |||
state.passwords = suggestions.concat(state.passwords || []); | |||
const siteWithoutWWW = site.replace(/^www./g, ""); | |||
for (let i = 0; i < passwords.length; i++) { | |||
const password = passwords[i]; | |||
if (site.endsWith(password.site)) { | |||
state.password = { ...password }; | |||
break; | |||
} else if (password.site.endsWith(siteWithoutWWW)) { | |||
state.password = { ...password }; | |||
} | |||
} | |||
}, | |||
[types.SET_MESSAGE](state, { message }) { | |||
state.message = message; | |||
@@ -129,9 +129,9 @@ test("SET_BASE_URL", () => { | |||
expect(state.baseURL).toBe(baseURL); | |||
}); | |||
test("ADD_SUGGESTIONS create 2 suggestions", () => { | |||
test("LOAD_PASSWORD_PROFILE", () => { | |||
const state = { | |||
defaultPassword: { | |||
password: { | |||
login: "", | |||
site: "", | |||
uppercase: true, | |||
@@ -179,17 +179,7 @@ test("ADD_SUGGESTIONS create 2 suggestions", () => { | |||
length: 12, | |||
version: 1 | |||
} | |||
] | |||
}; | |||
const ADD_SUGGESTIONS = mutations[types.ADD_SUGGESTIONS]; | |||
ADD_SUGGESTIONS(state, { site: "www.example.org" }); | |||
expect(state.passwords.length).toEqual(5); | |||
expect(state.passwords[0].site).toEqual("example"); | |||
expect(state.passwords[1].site).toEqual("example.org"); | |||
}); | |||
test("ADD_SUGGESTIONS no passwords", () => { | |||
const state = { | |||
], | |||
defaultPassword: { | |||
login: "", | |||
site: "", | |||
@@ -201,14 +191,132 @@ test("ADD_SUGGESTIONS no passwords", () => { | |||
counter: 1, | |||
version: 2 | |||
}, | |||
lastUse: null | |||
}; | |||
const LOAD_PASSWORD_PROFILE = mutations[types.LOAD_PASSWORD_PROFILE]; | |||
LOAD_PASSWORD_PROFILE(state, { site: "www.example.org" }); | |||
expect(state.password).toEqual(state.passwords[1]); | |||
}); | |||
test("LOAD_PASSWORD_PROFILE do nothing if id not empty", () => { | |||
const state = { | |||
password: { | |||
id: "1", | |||
site: "example.org" | |||
}, | |||
passwords: [] | |||
}; | |||
const LOAD_PASSWORD_PROFILE = mutations[types.LOAD_PASSWORD_PROFILE]; | |||
LOAD_PASSWORD_PROFILE(state, { site: "lesspass.com" }); | |||
expect(state.password.site).toBe("example.org"); | |||
}); | |||
test("LOAD_PASSWORD_PROFILE with passwords", () => { | |||
const state = { | |||
password: { | |||
site: "" | |||
}, | |||
passwords: [ | |||
{ id: "1", site: "www.example.org" }, | |||
{ id: "2", site: "www.google.com" } | |||
] | |||
}; | |||
const LOAD_PASSWORD_PROFILE = mutations[types.LOAD_PASSWORD_PROFILE]; | |||
LOAD_PASSWORD_PROFILE(state, { site: "www.google.com" }); | |||
expect(state.password.id).toBe("2"); | |||
expect(state.password.site).toBe("www.google.com"); | |||
}); | |||
test("LOAD_PASSWORD_PROFILE with no site keep password profile", () => { | |||
const state = { | |||
password: { | |||
id: "1", | |||
site: "example.org", | |||
login: "contact@example.org", | |||
length: 8, | |||
version: 2 | |||
}, | |||
passwords: [] | |||
}; | |||
const LOAD_PASSWORD_PROFILE = mutations[types.LOAD_PASSWORD_PROFILE]; | |||
LOAD_PASSWORD_PROFILE(state, { site: "" }); | |||
expect(state.password.id).toBe("1"); | |||
expect(state.password.site).toBe("example.org"); | |||
expect(state.password.login).toBe("contact@example.org"); | |||
expect(state.password.length).toBe(8); | |||
expect(state.password.version).toBe(2); | |||
}); | |||
test("LOAD_PASSWORD_PROFILE no passwords", () => { | |||
const state = { | |||
password: { | |||
site: "" | |||
}, | |||
passwords: [] | |||
}; | |||
const ADD_SUGGESTIONS = mutations[types.ADD_SUGGESTIONS]; | |||
ADD_SUGGESTIONS(state, { site: "www.example.org" }); | |||
expect(state.passwords.length).toBe(3); | |||
expect(state.passwords[0].site).toEqual("example"); | |||
expect(state.passwords[1].site).toEqual("example.org"); | |||
expect(state.passwords[2].site).toEqual("www.example.org"); | |||
const LOAD_PASSWORD_PROFILE = mutations[types.LOAD_PASSWORD_PROFILE]; | |||
LOAD_PASSWORD_PROFILE(state, { site: "account.google.com" }); | |||
expect(state.password.site).toBe("account.google.com"); | |||
}); | |||
test("LOAD_PASSWORD_PROFILE multiple accounts matching criteria", () => { | |||
const state = { | |||
password: { | |||
site: "" | |||
}, | |||
passwords: [ | |||
{ id: "1", site: "www.example.org" }, | |||
{ id: "2", site: "www.google.com" }, | |||
{ id: "3", site: "account.google.com" } | |||
] | |||
}; | |||
const LOAD_PASSWORD_PROFILE = mutations[types.LOAD_PASSWORD_PROFILE]; | |||
LOAD_PASSWORD_PROFILE(state, { site: "www.google.com" }); | |||
expect(state.password.id).toBe("2"); | |||
expect(state.password.site).toBe("www.google.com"); | |||
}); | |||
test("LOAD_PASSWORD_PROFILE multiple accounts matching criteria order doesn't matter", () => { | |||
const state = { | |||
password: { | |||
site: "" | |||
}, | |||
passwords: [ | |||
{ id: "1", site: "www.example.org" }, | |||
{ id: "2", site: "account.google.com" }, | |||
{ id: "3", site: "www.google.com" } | |||
] | |||
}; | |||
const LOAD_PASSWORD_PROFILE = mutations[types.LOAD_PASSWORD_PROFILE]; | |||
LOAD_PASSWORD_PROFILE(state, { site: "www.google.com" }); | |||
expect(state.password.id).toBe("3"); | |||
expect(state.password.site).toBe("www.google.com"); | |||
}); | |||
test("LOAD_PASSWORD_PROFILE ends matching criteria nrt #285", () => { | |||
const state = { | |||
password: { | |||
site: "" | |||
}, | |||
passwords: [{ id: "1", site: "account.google.com" }] | |||
}; | |||
const LOAD_PASSWORD_PROFILE = mutations[types.LOAD_PASSWORD_PROFILE]; | |||
LOAD_PASSWORD_PROFILE(state, { site: "www.google.com" }); | |||
expect(state.password.id).toBe("1"); | |||
expect(state.password.site).toBe("account.google.com"); | |||
}); | |||
test("LOAD_PASSWORD_PROFILE without www", () => { | |||
const state = { | |||
password: { | |||
site: "" | |||
}, | |||
passwords: [{ id: "1", site: "reddit.com" }] | |||
}; | |||
const LOAD_PASSWORD_PROFILE = mutations[types.LOAD_PASSWORD_PROFILE]; | |||
LOAD_PASSWORD_PROFILE(state, { site: "www.reddit.com" }); | |||
expect(state.password.id).toBe("1"); | |||
expect(state.password.site).toBe("reddit.com"); | |||
}); | |||
test("SET_SITE default state", () => { | |||
@@ -127,18 +127,18 @@ export default { | |||
}, | |||
computed: { | |||
...mapState({ | |||
password: state => ({ | |||
...state.password, | |||
login: state.password.login || state.defaultPassword.login | |||
}), | |||
passwords: state => state.passwords | |||
password: state => { | |||
state.password.login == state.password.login || state.defaultPassword.login | |||
return state.password | |||
}, | |||
passwords: state => state.passwords, | |||
}), | |||
...mapGetters(["passwordURL", "isDefaultProfile"]) | |||
}, | |||
beforeMount() { | |||
this.$store.dispatch("getPasswords").then(() => { | |||
urlParser.getSite().then(site => { | |||
this.$store.dispatch("addSuggestions", { site }); | |||
this.$store.dispatch("loadPasswordProfile", { site }); | |||
}); | |||
this.$store.dispatch("getPasswordFromUrlQuery", { | |||
query: this.$route.query | |||
@@ -234,18 +234,16 @@ export default { | |||
}); | |||
}, | |||
focusBestInputField() { | |||
const site = this.$refs.site; | |||
const login = this.$refs.login; | |||
const masterPassword = this.$refs.masterPassword; | |||
if (site && login && masterPassword){ | |||
const siteField = site.$refs.siteField; | |||
if (siteField.value && login.value){ | |||
return void masterPassword.$refs.passwordField.focus(); | |||
} | |||
if (siteField.value){ | |||
return void login.focus(); | |||
} | |||
return void siteField.focus(); | |||
try { | |||
const site = this.$refs.site.$refs.siteField; | |||
const login = this.$refs.login; | |||
const masterPassword = this.$refs.masterPassword; | |||
if (site && !site.value) return void site.focus(); | |||
if (login && !login.value) return void login.focus(); | |||
masterPassword.$refs.passwordField.focus(); | |||
} | |||
catch(err) { | |||
console.error("Can't focus password field") | |||
} | |||
}, | |||
copyPassword() { | |||