commit
33394e4d3e
7 changed files with 0 additions and 478 deletions
|
@ -1,18 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
mocha.setup({globals: ['google*']})
|
|
||||||
import './nonprofits'
|
|
||||||
|
|
||||||
/*
|
|
||||||
window.$ = require("jquery")
|
|
||||||
window.jQuery = window.$
|
|
||||||
window.domify = require("domify")
|
|
||||||
window.app = {}
|
|
||||||
require("../../app/assets/javascripts/common/vendor/jquery.cookie")
|
|
||||||
|
|
||||||
$(document).ready(function(){
|
|
||||||
window.appl = require("../../app/assets/javascripts/common/application_view")
|
|
||||||
require("./common/utilities-spec")
|
|
||||||
require("./nonprofits/donate/wizard-spec")
|
|
||||||
require("./nonprofits/donate/amount-step-spec")
|
|
||||||
})
|
|
||||||
*/
|
|
|
@ -1,11 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
window.$ = require("jquery")
|
|
||||||
window.jQuery = window.$
|
|
||||||
window.domify = require("domify")
|
|
||||||
window.app = {}
|
|
||||||
require("../../../app/assets/javascripts/common/vendor/jquery.cookie")
|
|
||||||
|
|
||||||
$(document).ready(function(){
|
|
||||||
window.appl = require("../../../app/assets/javascripts/common/application_view")
|
|
||||||
require("./utilities_spec")
|
|
||||||
})
|
|
|
@ -1,108 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
var utils = require("../../../app/assets/javascripts/common/utilities")
|
|
||||||
|
|
||||||
var fruit = { name: "banana", color: "yellow", flavor: "sweet" }
|
|
||||||
var vegetable = { name: "corn", color: "yellow", season: "summer"}
|
|
||||||
|
|
||||||
describe("utils.vals", function() {
|
|
||||||
it("takes an object and returns an array of values", function() {
|
|
||||||
expect(utils.vals(fruit)).toEqual(["banana", "yellow", "sweet"])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.keys", function() {
|
|
||||||
it("takes an object and returns an array of keys", function() {
|
|
||||||
expect(utils.keys(fruit)).toEqual(["name", "color", "flavor"])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.zero_pad", function() {
|
|
||||||
it("takes an initial number and the desired length of the number \
|
|
||||||
and returns the initial number with with zero's prepended to it", function() {
|
|
||||||
expect(utils.zero_pad(666, 10)).toBe("0000000666")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.simple_date_from_string", function() {
|
|
||||||
it("takes a loose string representation of a date and \
|
|
||||||
returns a uniform representaion", function() {
|
|
||||||
expect(utils.simple_date_from_string("Sun, 27 Sep 2015 12:00:00 UTC")).toBe("09/27/2015")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.number_with_comma", function() {
|
|
||||||
it("takes a number and returns a string with the number \
|
|
||||||
seperated by a comma at every three digits", function() {
|
|
||||||
expect(utils.number_with_commas(6666666666666)).toBe("6,666,666,666,666")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.merge", function() {
|
|
||||||
it("takes two objects and merges them (favors second object's \
|
|
||||||
values if the objects have same keys) ", function() {
|
|
||||||
expect(utils.merge(fruit, vegetable)).toEqual({name: "corn", color: "yellow", flavor: "sweet", season: "summer"})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.cents_to_dollars", function() {
|
|
||||||
it("takes a number representing an amount in cents and returns \
|
|
||||||
that amount representing a dollars", function() {
|
|
||||||
expect(utils.cents_to_dollars(666)).toBe("6.66")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.dollars_to_cents", function() {
|
|
||||||
it("takes a number representing an amount in dollars and returns \
|
|
||||||
that amount representing cents", function() {
|
|
||||||
expect(utils.dollars_to_cents(6.66)).toBe(666)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.trim", function() {
|
|
||||||
it("takes a string and removes any leading or trailing white space", function() {
|
|
||||||
expect(utils.trim(' whoa! ')).toBe('whoa!')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.flatten", function() {
|
|
||||||
it("takes an array of arrays and returns one flattened array", function() {
|
|
||||||
expect(utils.flatten([[1,2],[3,4]])).toEqual([1,2,3,4])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.uniq", function() {
|
|
||||||
it("takes an array and returns the array with no duplicates", function() {
|
|
||||||
expect(utils.uniq(['beer', 'wine', 'beer', 'mescal', 'beer', 'wine'])).toEqual(['beer', 'wine', 'mescal'])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.address_with_commas", function() {
|
|
||||||
it("takes a street, address and state and return them seperated by commas", function() {
|
|
||||||
expect(utils.address_with_commas('1600 Pennsylvania Ave NW', 'Washington', 'DC' )).toEqual('1600 Pennsylvania Ave NW, Washington, DC')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// pending...
|
|
||||||
|
|
||||||
xdescribe("utils.get_param", function() {
|
|
||||||
var location = {}
|
|
||||||
|
|
||||||
beforeAll(function() {
|
|
||||||
location.search = '?id=666'
|
|
||||||
})
|
|
||||||
afterAll(function() {
|
|
||||||
location.search = ''
|
|
||||||
})
|
|
||||||
xit("returns url params as a string", function() {
|
|
||||||
expect(utils.get_param('id')).toEqual('666')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
xdescribe("utils.toFormData", function() {
|
|
||||||
xit("takes a form and returns an object using the form inputs' attribute names as keys ", function() {
|
|
||||||
expect(utils.foFormData(form_object)).toBe('....')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
var utils = require("../../../app/assets/javascripts/common/utilities")
|
|
||||||
|
|
||||||
var fruit = { name: "banana", color: "yellow", flavor: "sweet" }
|
|
||||||
var vegetable = { name: "corn", color: "yellow", season: "summer"}
|
|
||||||
|
|
||||||
describe("utils.vals", function() {
|
|
||||||
it("takes an object and returns an array of values", function() {
|
|
||||||
expect(utils.vals(fruit)).toEqual(["banana", "yellow", "sweet"])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.keys", function() {
|
|
||||||
it("takes an object and returns an array of keys", function() {
|
|
||||||
expect(utils.keys(fruit)).toEqual(["name", "color", "flavor"])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.zero_pad", function() {
|
|
||||||
it("takes an initial number and the desired length of the number \
|
|
||||||
and returns the initial number with with zero's prepended to it", function() {
|
|
||||||
expect(utils.zero_pad(666, 10)).toBe("0000000666")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.simple_date_from_string", function() {
|
|
||||||
it("takes a loose string representation of a date and \
|
|
||||||
returns a uniform representaion", function() {
|
|
||||||
expect(utils.simple_date_from_string("Sun, 27 Sep 2015 00:00:00 UTC")).toBe("09/27/2015")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.number_with_comma", function() {
|
|
||||||
it("takes a number and returns a string with the number \
|
|
||||||
seperated by a comma at every three digits", function() {
|
|
||||||
expect(utils.number_with_commas(6666666666666)).toBe("6,666,666,666,666")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.merge", function() {
|
|
||||||
it("takes two objects and merges them (favors second object's \
|
|
||||||
values if the objects have same keys) ", function() {
|
|
||||||
expect(utils.merge(fruit, vegetable)).toEqual({name: "corn", color: "yellow", flavor: "sweet", season: "summer"})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.cents_to_dollars", function() {
|
|
||||||
it("takes a number representing an amount in cents and returns \
|
|
||||||
that amount representing a dollars", function() {
|
|
||||||
expect(utils.cents_to_dollars(666)).toBe("6.66")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.dollars_to_cents", function() {
|
|
||||||
it("takes a number representing an amount in dollars and returns \
|
|
||||||
that amount representing cents", function() {
|
|
||||||
expect(utils.dollars_to_cents(6.66)).toBe(666)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.trim", function() {
|
|
||||||
it("takes a string and removes any leading or trailing white space", function() {
|
|
||||||
expect(utils.trim(' whoa! ')).toBe('whoa!')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.flatten", function() {
|
|
||||||
it("takes an array of arrays and returns one flattened array", function() {
|
|
||||||
expect(utils.flatten([[1,2],[3,4]])).toEqual([1,2,3,4])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.uniq", function() {
|
|
||||||
it("takes an array and returns the array with no duplicates", function() {
|
|
||||||
expect(utils.uniq(['beer', 'wine', 'beer', 'mescal', 'beer', 'wine'])).toEqual(['beer', 'wine', 'mescal'])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("utils.address_with_commas", function() {
|
|
||||||
it("takes a street, address and state and return them seperated by commas", function() {
|
|
||||||
expect(utils.address_with_commas('1600 Pennsylvania Ave NW', 'Washington', 'DC' )).toEqual('1600 Pennsylvania Ave NW, Washington, DC')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
// pending...
|
|
||||||
|
|
||||||
xdescribe("utils.get_param", function() {
|
|
||||||
var location = {}
|
|
||||||
|
|
||||||
beforeAll(function() {
|
|
||||||
location.search = '?id=666'
|
|
||||||
})
|
|
||||||
afterAll(function() {
|
|
||||||
location.search = ''
|
|
||||||
})
|
|
||||||
xit("returns url params as a string", function() {
|
|
||||||
expect(utils.get_param('id')).toEqual('666')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
xdescribe("utils.toFormData", function() {
|
|
||||||
xit("takes a form and returns an object using the form inputs' attribute names as keys ", function() {
|
|
||||||
expect(utils.foFormData(form_object)).toBe('....')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
const snabbdom = require('snabbdom')
|
|
||||||
const flyd = require('flyd')
|
|
||||||
const render = require('ff-core/render')
|
|
||||||
const amount = require("../../../../client/js/nonprofits/donate/amount-step")
|
|
||||||
const R = require('ramda')
|
|
||||||
const assert = require('assert')
|
|
||||||
|
|
||||||
window.log = x => y => console.log(x,y)
|
|
||||||
window.app = {
|
|
||||||
nonprofit: {
|
|
||||||
id: 1
|
|
||||||
, name: 'test npo'
|
|
||||||
, logo: { normal: {url: 'xyz.com'} }
|
|
||||||
, tagline: 'whasup'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = snabbdom.init([
|
|
||||||
require('snabbdom/modules/eventlisteners')
|
|
||||||
, require('snabbdom/modules/class')
|
|
||||||
, require('snabbdom/modules/props')
|
|
||||||
, require('snabbdom/modules/style')
|
|
||||||
])
|
|
||||||
|
|
||||||
const init = (donationDefaults, params$) => {
|
|
||||||
let div = document.createElement('div')
|
|
||||||
let state = amount.init(donationDefaults||{}, params$||flyd.stream({}))
|
|
||||||
let streams = render({
|
|
||||||
container: div
|
|
||||||
, state: state
|
|
||||||
, patch: patch
|
|
||||||
, view: amount.view
|
|
||||||
})
|
|
||||||
streams.state = state
|
|
||||||
return streams
|
|
||||||
}
|
|
||||||
|
|
||||||
const allText = R.map(R.prop('textContent'))
|
|
||||||
const defaultDesigOptions = ['Choose a designation (optional)', 'Use my donation where most needed']
|
|
||||||
|
|
||||||
suite("donate wiz / amount step")
|
|
||||||
test("shows a designation dropdown if the multiple_designations param is set", ()=> {
|
|
||||||
let streams = init({}, flyd.stream({multiple_designations: ['a','b']}))
|
|
||||||
let options = allText(streams.dom$().querySelectorAll('.donate-designationDropdown option'))
|
|
||||||
assert.deepEqual(options, R.concat(defaultDesigOptions, ['a', 'b']))
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sets no designation with a dropdown on the default value', () => {
|
|
||||||
let streams = init({}, flyd.stream({multiple_designations: ['a', 'b']}))
|
|
||||||
let change = document.createEvent('Event')
|
|
||||||
change.initEvent('change', false, false, null )
|
|
||||||
let select = streams.dom$().querySelector('.donate-designationDropdown')
|
|
||||||
select.selectedIndex = 0
|
|
||||||
select.dispatchEvent(change)
|
|
||||||
assert.equal(streams.state.donation$().designation, '')
|
|
||||||
select.selectedIndex = 1
|
|
||||||
select.dispatchEvent(change)
|
|
||||||
assert.equal(streams.state.donation$().designation, '')
|
|
||||||
})
|
|
||||||
|
|
||||||
test("changing the dropdown sets the designation", () => {
|
|
||||||
let streams = init({}, flyd.stream({multiple_designations: ['a', 'b']}))
|
|
||||||
let change = document.createEvent('Event')
|
|
||||||
change.initEvent('change', false, false, null )
|
|
||||||
let select = streams.dom$().querySelector('.donate-designationDropdown')
|
|
||||||
select.selectedIndex = 2
|
|
||||||
select.dispatchEvent(change)
|
|
||||||
assert.equal(streams.state.donation$().designation, 'a')
|
|
||||||
})
|
|
||||||
|
|
||||||
test("shows no dropdown if the multiple_designations param is not set", ()=> {
|
|
||||||
let streams = init()
|
|
||||||
let drop = streams.dom$().querySelector('.donate-designationDropdown')
|
|
||||||
assert.equal(drop, null)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("shows a recurring donation checkbox by default", ()=> {
|
|
||||||
let streams = init()
|
|
||||||
assert(streams.dom$().querySelector('.donate-recurringCheckbox'))
|
|
||||||
})
|
|
||||||
|
|
||||||
test("hides the recurring donation checkbox if params type is set to recurring", ()=> {
|
|
||||||
let streams = init({}, flyd.stream({type: 'recurring'}))
|
|
||||||
let check = streams.dom$().querySelector('.donate-recurringCheckbox')
|
|
||||||
assert.equal(check, null)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("shows a recurring message if the recurring box is checked", ()=> {
|
|
||||||
let streams = init()
|
|
||||||
let change = document.createEvent('Event')
|
|
||||||
change.initEvent('change', false, false, null )
|
|
||||||
streams.dom$().querySelector('.donate-recurringCheckbox input').dispatchEvent(change)
|
|
||||||
const msg = streams.dom$().querySelector('.donate-recurringMessage').textContent
|
|
||||||
assert.equal(msg, 'Select an amount for your monthly contribution')
|
|
||||||
})
|
|
||||||
|
|
||||||
test("shows a recurring message if the type in params is set to recurring", ()=> {
|
|
||||||
let streams = init({}, flyd.stream({type: 'recurring'}))
|
|
||||||
const msg = streams.dom$().querySelector('.donate-recurringMessage').textContent
|
|
||||||
assert.equal(msg, 'Select an amount for your monthly contribution')
|
|
||||||
})
|
|
||||||
|
|
||||||
test("does not show a recurring message if the type is one-time in params", ()=> {
|
|
||||||
let streams = init({}, flyd.stream({type: 'one-time'}))
|
|
||||||
const msg = streams.dom$().querySelector('.donate-recurringMessage')
|
|
||||||
assert.equal(msg, null)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("does not show a recurring message if the type is one-time in params", ()=> {
|
|
||||||
let streams = init({}, flyd.stream({type: 'one-time'}))
|
|
||||||
const msg = streams.dom$().querySelector('.donate-recurringCheckbox')
|
|
||||||
assert.equal(msg, null)
|
|
||||||
})
|
|
|
@ -1,116 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
const snabbdom = require('snabbdom')
|
|
||||||
const flyd = require('flyd')
|
|
||||||
const render = require('ff-core/render')
|
|
||||||
const wiz = require("../../../../client/js/nonprofits/donate/wizard")
|
|
||||||
const R = require('ramda')
|
|
||||||
const assert = require('assert')
|
|
||||||
|
|
||||||
window.log = x => y => console.log(x,y)
|
|
||||||
window.app = {
|
|
||||||
nonprofit: {
|
|
||||||
id: 1
|
|
||||||
, name: 'test npo'
|
|
||||||
, logo: { normal: {url: 'xyz.com'} }
|
|
||||||
, tagline: 'whasup'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const patch = snabbdom.init([
|
|
||||||
require('snabbdom/modules/eventlisteners')
|
|
||||||
, require('snabbdom/modules/class')
|
|
||||||
, require('snabbdom/modules/props')
|
|
||||||
, require('snabbdom/modules/style')
|
|
||||||
])
|
|
||||||
|
|
||||||
const init = params$=> {
|
|
||||||
params$ = params$ || flyd.stream({})
|
|
||||||
let div = document.createElement('div')
|
|
||||||
let state = wiz.init(params$)
|
|
||||||
let streams = render({
|
|
||||||
container: div
|
|
||||||
, state: state
|
|
||||||
, patch: patch
|
|
||||||
, view: wiz.view
|
|
||||||
})
|
|
||||||
streams.state = state
|
|
||||||
return streams
|
|
||||||
}
|
|
||||||
|
|
||||||
suite("donate wizzzzz")
|
|
||||||
test("initializes amount, info, and payment steps", ()=> {
|
|
||||||
let streams = init()
|
|
||||||
let labels = streams.dom$().querySelectorAll('.ff-wizard-index-label')
|
|
||||||
assert.deepEqual(R.map(R.prop('textContent'), labels), ['Amount', 'Info', 'Payment'])
|
|
||||||
})
|
|
||||||
|
|
||||||
test("shows the nonprofit name without a campaign", () => {
|
|
||||||
let streams = init()
|
|
||||||
let title = streams.dom$().querySelector('.titleRow-info h2').textContent
|
|
||||||
assert.equal(title, app.nonprofit.name)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("shows the campaign name with a campaign", () => {
|
|
||||||
let streams = init()
|
|
||||||
let title = streams.dom$().querySelector('.titleRow-info h2').textContent
|
|
||||||
assert.equal(title, app.nonprofit.name)
|
|
||||||
})
|
|
||||||
|
|
||||||
test("shows the campaign tagline with a campaign", () => {
|
|
||||||
app.campaign = {name: 'campaignxyz', id: 1}
|
|
||||||
let streams = init()
|
|
||||||
let title = streams.dom$().querySelector('.titleRow-info h2').textContent
|
|
||||||
assert.equal(title, app.campaign.name)
|
|
||||||
app.campaign = {}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('adds .is-modal class if state.params.offsite$()', ()=> {
|
|
||||||
let streams = init(flyd.stream({offsite: true}))
|
|
||||||
assert.equal(streams.dom$().className.indexOf('is-modal'), 0)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shows the tagline if no designation and no single amount', ()=> {
|
|
||||||
let streams = init()
|
|
||||||
assert.equal(streams.dom$().querySelector('.titleRow-info p').textContent, app.nonprofit.tagline)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shows the designation if designation param set and no single amount', ()=> {
|
|
||||||
const designation = '1312312xyz'
|
|
||||||
let streams = init(flyd.stream({designation}))
|
|
||||||
assert.equal(streams.dom$().querySelector('.titleRow-info p').textContent, ` Designation: ${designation}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shows the designation description if it is set and designation param set and no single amount', ()=> {
|
|
||||||
const designation = '1312312xyz'
|
|
||||||
const designation_desc = 'desc23923943'
|
|
||||||
let streams = init(flyd.stream({designation, designation_desc}))
|
|
||||||
assert.equal(streams.dom$().querySelector('.titleRow-info p').textContent, ` Designation: ${designation}${designation_desc}`)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shows the tagline if designation param set and single amount set', ()=> {
|
|
||||||
const designation = '1312312xyz'
|
|
||||||
let streams = init(flyd.stream({designation, single_amount: 1000}))
|
|
||||||
assert.equal(streams.dom$().querySelector('.titleRow-info p').textContent, app.nonprofit.tagline)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('hides the footer if no user is in the env', () => {
|
|
||||||
let streams = init()
|
|
||||||
const idx = streams.dom$().querySelector('.donateForm-footer').className.indexOf('hide')
|
|
||||||
assert.notEqual(idx, -1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shows the footer if a user is in the env', () => {
|
|
||||||
app.user = {email: 'user@example.com', id: 1}
|
|
||||||
let streams = init()
|
|
||||||
const idx = streams.dom$().querySelector('.donateForm-footer').className.indexOf('hide')
|
|
||||||
assert.equal(idx, -1)
|
|
||||||
app.user = {}
|
|
||||||
})
|
|
||||||
|
|
||||||
test('shows user info text if a user is in the env', () => {
|
|
||||||
app.user = {email: 'user@example.com', id: 1}
|
|
||||||
let streams = init()
|
|
||||||
const text = streams.dom$().querySelector('.donateForm-footer').textContent
|
|
||||||
assert.equal(text, 'Signed in as user@example.com Logout')
|
|
||||||
app.user = {}
|
|
||||||
})
|
|
|
@ -1,3 +0,0 @@
|
||||||
// License: LGPL-3.0-or-later
|
|
||||||
import './donate/amount-step-spec'
|
|
||||||
import './donate/wizard-spec'
|
|
Loading…
Reference in a new issue