mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-12 07:34:52 +08:00
tests(calendar): adding calendar and scheduler tests
Summary: Adding tests Test Plan: Tests Reviewers: juan, bengotow Reviewed By: bengotow Differential Revision: https://phab.nylas.com/D2892
This commit is contained in:
parent
36ab9d593b
commit
b29d5ac75b
28 changed files with 1521 additions and 71 deletions
|
@ -1,4 +1,5 @@
|
|||
import React from 'react'
|
||||
import classnames from 'classnames'
|
||||
import SchedulerActions from '../scheduler-actions'
|
||||
import {CALENDAR_ID} from '../scheduler-constants'
|
||||
|
||||
|
@ -30,9 +31,14 @@ export default class ProposedTimeEvent extends React.Component {
|
|||
}
|
||||
|
||||
render() {
|
||||
const className = classnames({
|
||||
"rm-time": true,
|
||||
proposal: this.props.event.proposalType === "proposal",
|
||||
availability: this.props.event.proposalType === "availability",
|
||||
});
|
||||
if (this.props.event.calendarId === CALENDAR_ID) {
|
||||
return (
|
||||
<div className="rm-time"
|
||||
<div className={className}
|
||||
data-end={this.props.event.end}
|
||||
data-start={this.props.event.start}
|
||||
onMouseDown={this._onMouseDown}
|
||||
|
|
|
@ -67,7 +67,7 @@ export default class ProposedTimePicker extends React.Component {
|
|||
<button key="clear"
|
||||
style={{order: -99, marginLeft: 20}}
|
||||
onClick={this._onClearProposals}
|
||||
className="btn"
|
||||
className="btn clear-proposed-times"
|
||||
>
|
||||
Clear Times
|
||||
</button>
|
||||
|
@ -86,7 +86,11 @@ export default class ProposedTimePicker extends React.Component {
|
|||
const durationPicker = (
|
||||
<div key="dp" className="duration-picker" style={{order: -100}}>
|
||||
<label style={{paddingRight: 10}}>Event Duration:</label>
|
||||
<select value={this.state.duration.join("|")} onChange={this._onChangeDuration}>
|
||||
<select
|
||||
className="duration-picker-select"
|
||||
value={this.state.duration.join("|")}
|
||||
onChange={this._onChangeDuration}
|
||||
>
|
||||
{optComponents}
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
@ -94,6 +94,7 @@ export default class NewEventCardContainer extends Component {
|
|||
if (this._session && this.state.event) {
|
||||
card = (
|
||||
<NewEventCard event={this.state.event}
|
||||
ref="newEventCard"
|
||||
draft={this._session.draft()}
|
||||
onRemove={this._removeEvent}
|
||||
onChange={this._updateEvent}
|
||||
|
|
|
@ -223,6 +223,7 @@ export default class NewEventCard extends React.Component {
|
|||
{this._renderIcon("ic-eventcard-description@2x.png")}
|
||||
<input type="text"
|
||||
name="title"
|
||||
className="event-title"
|
||||
placeholder="Add an event title"
|
||||
value={this.props.event.title || ""}
|
||||
onChange={e => this.props.onChange({title: e.target.value}) }
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import moment from 'moment'
|
||||
import {PLUGIN_ID} from '../scheduler-constants'
|
||||
|
||||
import {
|
||||
Event,
|
||||
Calendar,
|
||||
DatabaseStore,
|
||||
} from 'nylas-exports'
|
||||
|
||||
export default class NewEventHelper {
|
||||
|
||||
// Extra level of indirection for testing
|
||||
static now() {
|
||||
return moment()
|
||||
}
|
||||
|
||||
static addEventToSession(session) {
|
||||
if (!session) { return }
|
||||
const draft = session.draft()
|
||||
DatabaseStore.findAll(Calendar, {accountId: draft.accountId})
|
||||
.then((allCalendars) => {
|
||||
if (allCalendars.length === 0) {
|
||||
throw new Error(`Can't create an event. The Account \
|
||||
${draft.accountId} has no calendars.`);
|
||||
}
|
||||
|
||||
const cals = allCalendars.filter(c => !c.readOnly);
|
||||
|
||||
if (cals.length === 0) {
|
||||
NylasEnv.showErrorDialog(`This account has no editable \
|
||||
calendars. We can't create an event for you. Please make sure you have an \
|
||||
editable calendar with your account provider.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const start = NewEventHelper.now().ceil(30, 'minutes');
|
||||
const metadata = draft.metadataForPluginId(PLUGIN_ID) || {};
|
||||
metadata.uid = draft.clientId;
|
||||
metadata.pendingEvent = new Event({
|
||||
calendarId: cals[0].id,
|
||||
start: start.unix(),
|
||||
end: moment(start).add(1, 'hour').unix(),
|
||||
}).toJSON();
|
||||
session.changes.addPluginMetadata(PLUGIN_ID, metadata);
|
||||
})
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ export default class NewEventPreview extends React.Component {
|
|||
paddingLeft: "40px",
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<div className="new-event-preview">
|
||||
<h2 style={styles}>
|
||||
{this._renderB64Img("description", {verticalAlign: "middle"})}
|
||||
{this.props.event.title}
|
||||
|
|
|
@ -199,7 +199,15 @@ export default class ProposedTimeList extends React.Component {
|
|||
)
|
||||
}
|
||||
|
||||
return <table style={this._sProposalTable()}><tbody>{trs}</tbody></table>
|
||||
return (
|
||||
<table style={this._sProposalTable()}
|
||||
className="proposed-time-table"
|
||||
>
|
||||
<tbody>
|
||||
{trs}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
_renderProposalTimeText(proposal) {
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import {
|
||||
Event,
|
||||
Actions,
|
||||
Calendar,
|
||||
APIError,
|
||||
NylasAPI,
|
||||
DraftStore,
|
||||
DatabaseStore,
|
||||
} from 'nylas-exports'
|
||||
import {Menu, RetinaImg} from 'nylas-component-kit'
|
||||
import {PLUGIN_ID, PLUGIN_NAME} from '../scheduler-constants'
|
||||
|
||||
import NewEventHelper from './new-event-helper'
|
||||
|
||||
import moment from 'moment'
|
||||
// moment-round upon require patches `moment` with new functions.
|
||||
require('moment-round')
|
||||
|
@ -70,7 +69,7 @@ export default class SchedulerComposerButton extends React.Component {
|
|||
// Helper method that will render the contents of our popover.
|
||||
_renderPopover() {
|
||||
const headerComponents = [
|
||||
<span>I'd like to:</span>,
|
||||
<span key="header">I'd like to:</span>,
|
||||
];
|
||||
const items = [
|
||||
MEETING_REQUEST,
|
||||
|
@ -91,7 +90,7 @@ export default class SchedulerComposerButton extends React.Component {
|
|||
}
|
||||
|
||||
_onSelectItem = (item) => {
|
||||
this._onCreateEventCard();
|
||||
NewEventHelper.addEventToSession(this._session)
|
||||
const draft = this._session.draft()
|
||||
if (item === PROPOSAL) {
|
||||
NylasEnv.newWindow({
|
||||
|
@ -131,42 +130,15 @@ Please try again later.\n\nError: ${error}`
|
|||
)
|
||||
}
|
||||
|
||||
_onCreateEventCard = () => {
|
||||
if (!this._session) { return }
|
||||
const draft = this._session.draft()
|
||||
DatabaseStore.findAll(Calendar, {accountId: draft.accountId})
|
||||
.then((allCalendars) => {
|
||||
if (allCalendars.length === 0) {
|
||||
throw new Error(`Can't create an event. The Account \
|
||||
${draft.accountId} has no calendars.`);
|
||||
}
|
||||
|
||||
const cals = allCalendars.filter(c => !c.readOnly);
|
||||
|
||||
if (cals.length === 0) {
|
||||
NylasEnv.showErrorDialog(`This account has no editable \
|
||||
calendars. We can't create an event for you. Please make sure you have an \
|
||||
editable calendar with your account provider.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const start = moment().ceil(30, 'minutes');
|
||||
const metadata = draft.metadataForPluginId(PLUGIN_ID) || {};
|
||||
metadata.uid = draft.clientId;
|
||||
metadata.pendingEvent = new Event({
|
||||
calendarId: cals[0].id,
|
||||
start: start.unix(),
|
||||
end: moment(start).add(1, 'hour').unix(),
|
||||
}).toJSON();
|
||||
Actions.setMetadata(draft, PLUGIN_ID, metadata);
|
||||
})
|
||||
_now() {
|
||||
return moment()
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<button className={`btn btn-toolbar ${this.state.enabled ? "btn-enabled" : ""}`}
|
||||
onClick={this._onClick}
|
||||
title="Add an event…"
|
||||
title="Schedule an event…"
|
||||
>
|
||||
<RetinaImg url="nylas://composer-scheduler/assets/ic-composer-scheduler@2x.png"
|
||||
mode={RetinaImg.Mode.ContentIsMask}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React from 'react'
|
||||
import ReactDOMServer from 'react-dom/server'
|
||||
import {PLUGIN_ID} from '../scheduler-constants'
|
||||
import NewEventPreview from './new-event-preview'
|
||||
import ProposedTimeList from './proposed-time-list'
|
||||
|
@ -91,7 +92,7 @@ export default class SchedulerComposerExtension extends ComposerExtension {
|
|||
inEmail: true,
|
||||
proposals: metadata.proposals,
|
||||
});
|
||||
const markup = React.renderToStaticMarkup(el);
|
||||
const markup = ReactDOMServer.renderToStaticMarkup(el);
|
||||
const nextBody = SchedulerComposerExtension._insertInBody(nextDraft.body, markup)
|
||||
nextDraft.body = nextBody;
|
||||
} else {
|
||||
|
@ -99,7 +100,7 @@ export default class SchedulerComposerExtension extends ComposerExtension {
|
|||
{
|
||||
event: metadata.pendingEvent,
|
||||
});
|
||||
const markup = React.renderToStaticMarkup(el);
|
||||
const markup = ReactDOMServer.renderToStaticMarkup(el);
|
||||
const nextBody = SchedulerComposerExtension._insertInBody(nextDraft.body, markup)
|
||||
nextDraft.body = nextBody;
|
||||
}
|
||||
|
|
|
@ -45,10 +45,10 @@ export function serialize() {
|
|||
export function deactivate() {
|
||||
if (NylasEnv.getWindowType() === 'calendar') {
|
||||
ProposedTimeCalendarStore.deactivate()
|
||||
ProposedTimeMainWindowStore.deactivate()
|
||||
ComponentRegistry.unregister(ProposedTimeEvent);
|
||||
ComponentRegistry.unregister(ProposedTimePicker);
|
||||
} else {
|
||||
ProposedTimeMainWindowStore.deactivate()
|
||||
ComponentRegistry.unregister(NewEventCardContainer);
|
||||
ComponentRegistry.unregister(SchedulerComposerButton);
|
||||
ExtensionRegistry.Composer.unregister(SchedulerComposerExtension);
|
||||
|
|
|
@ -56,8 +56,8 @@ class ProposedTimeCalendarStore extends NylasStore {
|
|||
if (!this._dragBuffer.anchor) {
|
||||
return []
|
||||
}
|
||||
const {start, end} = this._dragBuffer
|
||||
return [new Event().fromJSON({
|
||||
const {start, end} = this._dragBuffer;
|
||||
const event = new Event().fromJSON({
|
||||
title: "Availability Block",
|
||||
calendar_id: CALENDAR_ID,
|
||||
when: {
|
||||
|
@ -65,12 +65,14 @@ class ProposedTimeCalendarStore extends NylasStore {
|
|||
start_time: start,
|
||||
end_time: end,
|
||||
},
|
||||
})];
|
||||
})
|
||||
event.proposalType = "availability"
|
||||
return [event];
|
||||
}
|
||||
|
||||
proposalsAsEvents() {
|
||||
return _.map(this._proposals, (p) =>
|
||||
new Event().fromJSON({
|
||||
return _.map(this._proposals, (p) => {
|
||||
const event = new Event().fromJSON({
|
||||
title: "Proposed Time",
|
||||
calendar_id: CALENDAR_ID,
|
||||
when: {
|
||||
|
@ -79,7 +81,9 @@ class ProposedTimeCalendarStore extends NylasStore {
|
|||
end_time: p.end,
|
||||
},
|
||||
})
|
||||
).concat(this._dragBufferAsEvent());
|
||||
event.proposalType = "proposal";
|
||||
return event
|
||||
}).concat(this._dragBufferAsEvent());
|
||||
}
|
||||
|
||||
_convertBufferToProposedTimes() {
|
||||
|
@ -90,16 +94,19 @@ class ProposedTimeCalendarStore extends NylasStore {
|
|||
const maxMoment = moment.unix(bounds.end);
|
||||
maxMoment.ceil(30, 'minutes');
|
||||
|
||||
if (maxMoment.isSameOrBefore(minMoment)) { return }
|
||||
if (maxMoment.isSame(minMoment)) {
|
||||
maxMoment.add(30, 'minutes')
|
||||
}
|
||||
|
||||
const overlapBoundsTest = {start: bounds.start, end: bounds.end - 1}
|
||||
this._proposals = _.reject(this._proposals, (p) =>
|
||||
Utils.overlapsBounds(bounds, p)
|
||||
Utils.overlapsBounds(overlapBoundsTest, p)
|
||||
)
|
||||
|
||||
const blockSize = this._duration.slice(0, 2)
|
||||
blockSize[0] = blockSize[0] / 1; // moment requires a number
|
||||
const isMinBlockSize = (bounds.end - bounds.start) >= moment.duration.apply(moment, blockSize).as('seconds');
|
||||
while (minMoment.isSameOrBefore(maxMoment)) {
|
||||
while (minMoment.isBefore(maxMoment)) {
|
||||
const start = minMoment.unix();
|
||||
minMoment.add(blockSize[0], blockSize[1]);
|
||||
const end = minMoment.unix() - 1;
|
||||
|
|
|
@ -16,10 +16,12 @@ for (const key in SchedulerActions) {
|
|||
}
|
||||
}
|
||||
|
||||
NylasEnv.actionBridge.registerGlobalAction({
|
||||
scope: "SchedulerActions",
|
||||
name: "confirmChoices",
|
||||
actionFn: SchedulerActions.confirmChoices,
|
||||
});
|
||||
if (!NylasEnv.inSpecMode()) {
|
||||
NylasEnv.actionBridge.registerGlobalAction({
|
||||
scope: "SchedulerActions",
|
||||
name: "confirmChoices",
|
||||
actionFn: SchedulerActions.confirmChoices,
|
||||
});
|
||||
}
|
||||
|
||||
export default SchedulerActions
|
||||
|
|
|
@ -1,6 +1,14 @@
|
|||
import plugin from '../package.json'
|
||||
|
||||
export const PLUGIN_ID = plugin.appId[NylasEnv.config.get("env")];
|
||||
export const PLUGIN_URL = plugin.serverUrl[NylasEnv.config.get("env")];
|
||||
let pluginId = plugin.appId[NylasEnv.config.get("env")];
|
||||
let pluginUrl = plugin.serverUrl[NylasEnv.config.get("env")];
|
||||
|
||||
if (NylasEnv.inSpecMode()) {
|
||||
pluginId = "TEST_SCHEDULER_PLUGIN_ID"
|
||||
pluginUrl = "https://edgehill-test.nylas.com"
|
||||
}
|
||||
|
||||
export const PLUGIN_ID = pluginId;
|
||||
export const PLUGIN_URL = pluginUrl;
|
||||
export const PLUGIN_NAME = "Quick Schedule"
|
||||
export const CALENDAR_ID = "QUICK SCHEDULE"
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import {
|
||||
DatabaseStore,
|
||||
DraftStore,
|
||||
Message,
|
||||
Actions,
|
||||
Calendar,
|
||||
Contact,
|
||||
} from 'nylas-exports'
|
||||
import {activate, deactivate} from '../lib/main'
|
||||
import {PLUGIN_ID} from '../lib/scheduler-constants'
|
||||
|
||||
export const DRAFT_CLIENT_ID = "draft-client-id"
|
||||
|
||||
// Must be a `function` so `this` can be overridden by caller's `apply`
|
||||
export const prepareDraft = function prepareDraft() {
|
||||
spyOn(NylasEnv, "isMainWindow").andReturn(true);
|
||||
spyOn(NylasEnv, "getWindowType").andReturn("root");
|
||||
spyOn(Actions, "setMetadata").andCallFake((draft, pluginId, metadata) => {
|
||||
if (!this.session) {
|
||||
throw new Error("Setup test session first")
|
||||
}
|
||||
this.session.changes.addPluginMetadata(PLUGIN_ID, metadata);
|
||||
})
|
||||
activate();
|
||||
|
||||
const draft = new Message({
|
||||
clientId: DRAFT_CLIENT_ID,
|
||||
draft: true,
|
||||
body: "",
|
||||
accountId: window.TEST_ACCOUNT_ID,
|
||||
from: [new Contact({email: window.TEST_ACCOUNT_EMAIL})],
|
||||
})
|
||||
|
||||
spyOn(DatabaseStore, 'run').andReturn(Promise.resolve(draft));
|
||||
|
||||
this.session = null;
|
||||
runs(() => {
|
||||
DraftStore.sessionForClientId(DRAFT_CLIENT_ID).then((session) => {
|
||||
this.session = session
|
||||
});
|
||||
})
|
||||
waitsFor(() => this.session);
|
||||
}
|
||||
|
||||
export const cleanupDraft = function cleanupDraft() {
|
||||
DraftStore._cleanupAllSessions()
|
||||
deactivate()
|
||||
}
|
||||
|
||||
export const setupCalendars = function setupCalendars() {
|
||||
const aid = window.TEST_ACCOUNT_ID
|
||||
spyOn(DatabaseStore, "findAll").andCallFake((klass, {accountId}) => {
|
||||
expect(klass).toBe(Calendar);
|
||||
expect(accountId).toBe(aid);
|
||||
const cals = [
|
||||
new Calendar({accountId: aid, readOnly: false, name: 'a'}),
|
||||
new Calendar({accountId: aid, readOnly: true, name: 'b'}),
|
||||
]
|
||||
return Promise.resolve(cals);
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
import _ from 'underscore'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import {PLUGIN_ID} from '../lib/scheduler-constants'
|
||||
import NewEventCard from '../lib/composer/new-event-card'
|
||||
import ReactTestUtils from 'react-addons-test-utils';
|
||||
import NewEventCardContainer from '../lib/composer/new-event-card-container'
|
||||
|
||||
import {
|
||||
Calendar,
|
||||
Event,
|
||||
DatabaseStore,
|
||||
} from 'nylas-exports'
|
||||
|
||||
import {
|
||||
DRAFT_CLIENT_ID,
|
||||
prepareDraft,
|
||||
cleanupDraft,
|
||||
} from './composer-scheduler-spec-helper'
|
||||
|
||||
const now = window.testNowMoment
|
||||
|
||||
describe("NewEventCard", () => {
|
||||
beforeEach(() => {
|
||||
this.session = null
|
||||
// Will eventually fill this.session
|
||||
prepareDraft.call(this)
|
||||
|
||||
runs(() => {
|
||||
this.eventCardContainer = ReactTestUtils.renderIntoDocument(
|
||||
<NewEventCardContainer draftClientId={DRAFT_CLIENT_ID} />
|
||||
);
|
||||
})
|
||||
|
||||
waitsFor(() => this.eventCardContainer._session)
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupDraft()
|
||||
})
|
||||
|
||||
const testCalendars = () => [new Calendar({
|
||||
clientId: "client-1",
|
||||
servierId: "server-1",
|
||||
name: "Test Calendar",
|
||||
})]
|
||||
|
||||
const stubCalendars = (calendars = []) => {
|
||||
jasmine.unspy(DatabaseStore, "run")
|
||||
spyOn(DatabaseStore, "run").andCallFake((query) => {
|
||||
if (query.objectClass() === Calendar.name) {
|
||||
return Promise.resolve(calendars)
|
||||
}
|
||||
return Promise.resolve()
|
||||
})
|
||||
}
|
||||
|
||||
const setNewTestEvent = (callback) => {
|
||||
runs(() => {
|
||||
if (!this.session) {
|
||||
throw new Error("Setup test session first")
|
||||
}
|
||||
const metadata = {}
|
||||
metadata.uid = DRAFT_CLIENT_ID;
|
||||
metadata.pendingEvent = new Event({
|
||||
calendarId: "TEST_CALENDAR_ID",
|
||||
title: "",
|
||||
start: now().unix(),
|
||||
end: now().add(1, 'hour').unix(),
|
||||
}).toJSON();
|
||||
this.session.changes.addPluginMetadata(PLUGIN_ID, metadata);
|
||||
})
|
||||
|
||||
waitsFor(() => this.eventCardContainer.state.event);
|
||||
|
||||
runs(callback)
|
||||
}
|
||||
|
||||
it("creates a new event card", () => {
|
||||
const el = ReactTestUtils.findRenderedComponentWithType(this.eventCardContainer,
|
||||
NewEventCardContainer);
|
||||
expect(el instanceof NewEventCardContainer).toBe(true)
|
||||
});
|
||||
|
||||
it("doesn't render if there's no event on metadata", () => {
|
||||
expect(this.eventCardContainer.refs.newEventCard).not.toBeDefined();
|
||||
});
|
||||
|
||||
it("renders the event card when an event is created", () => {
|
||||
stubCalendars()
|
||||
setNewTestEvent(() => {
|
||||
expect(this.eventCardContainer.refs.newEventCard).toBeDefined();
|
||||
expect(this.eventCardContainer.refs.newEventCard instanceof NewEventCard).toBe(true);
|
||||
})
|
||||
});
|
||||
|
||||
it("loads the calendars for email", () => {
|
||||
stubCalendars(testCalendars())
|
||||
setNewTestEvent(() => { })
|
||||
waitsFor(() =>
|
||||
this.eventCardContainer.refs.newEventCard.state.calendars.length > 0
|
||||
)
|
||||
runs(() => {
|
||||
const newEventCard = this.eventCardContainer.refs.newEventCard;
|
||||
expect(newEventCard.state.calendars).toEqual(testCalendars());
|
||||
});
|
||||
});
|
||||
|
||||
it("removes the event and clears metadata", () => {
|
||||
stubCalendars(testCalendars())
|
||||
setNewTestEvent(() => {
|
||||
const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass,
|
||||
this.eventCardContainer);
|
||||
const rmBtn = ReactDOM.findDOMNode($("remove-button")[0]);
|
||||
|
||||
// The event is there before clicking remove
|
||||
expect(this.eventCardContainer.state.event).toBeDefined()
|
||||
expect(this.eventCardContainer.refs.newEventCard).toBeDefined()
|
||||
expect(this.session.draft().metadataForPluginId(PLUGIN_ID).pendingEvent).toBeDefined()
|
||||
|
||||
ReactTestUtils.Simulate.click(rmBtn);
|
||||
|
||||
// The event has been removed from metadata and state
|
||||
expect(this.eventCardContainer.state.event).toBe(null)
|
||||
expect(this.eventCardContainer.refs.newEventCard).not.toBeDefined()
|
||||
expect(this.session.draft().metadataForPluginId(PLUGIN_ID).pendingEvent).not.toBeDefined()
|
||||
})
|
||||
});
|
||||
|
||||
const getPendingEvent = () =>
|
||||
this.session.draft().metadataForPluginId(PLUGIN_ID).pendingEvent
|
||||
|
||||
it("properly updates the event", () => {
|
||||
stubCalendars(testCalendars())
|
||||
setNewTestEvent(() => {
|
||||
const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass,
|
||||
this.eventCardContainer);
|
||||
const title = ReactDOM.findDOMNode($("event-title")[0]);
|
||||
|
||||
// The event has the old title
|
||||
expect(this.eventCardContainer.state.event.title).toBe("")
|
||||
expect(getPendingEvent().title).toBe("")
|
||||
|
||||
title.value = "Test"
|
||||
ReactTestUtils.Simulate.change(title);
|
||||
|
||||
// The event has the new title
|
||||
expect(this.eventCardContainer.state.event.title).toBe("Test")
|
||||
expect(getPendingEvent().title).toBe("Test")
|
||||
})
|
||||
});
|
||||
|
||||
it("updates the day", () => {
|
||||
stubCalendars(testCalendars())
|
||||
setNewTestEvent(() => {
|
||||
const eventCard = this.eventCardContainer.refs.newEventCard;
|
||||
|
||||
// The event has the default day
|
||||
const nowUnix = now().unix()
|
||||
expect(this.eventCardContainer.state.event.start).toBe(nowUnix)
|
||||
expect(getPendingEvent()._start).toBe(nowUnix)
|
||||
|
||||
// The event has the new day
|
||||
const newDay = now().add(2, 'days');
|
||||
eventCard._onChangeDay(newDay.valueOf());
|
||||
|
||||
expect(this.eventCardContainer.state.event.start).toBe(newDay.unix())
|
||||
expect(getPendingEvent()._start).toBe(newDay.unix())
|
||||
})
|
||||
});
|
||||
|
||||
it("updates the time properly", () => {
|
||||
stubCalendars(testCalendars())
|
||||
setNewTestEvent(() => {
|
||||
const eventCard = this.eventCardContainer.refs.newEventCard;
|
||||
|
||||
const oldEnd = now().add(1, 'hour').unix()
|
||||
expect(this.eventCardContainer.state.event.start).toBe(now().unix())
|
||||
expect(getPendingEvent()._start).toBe(now().unix())
|
||||
expect(getPendingEvent()._end).toBe(oldEnd)
|
||||
|
||||
const newStart = now().subtract(1, 'hour');
|
||||
eventCard._onChangeStartTime(newStart.valueOf());
|
||||
|
||||
expect(this.eventCardContainer.state.event.start).toBe(newStart.unix())
|
||||
expect(getPendingEvent()._start).toBe(newStart.unix())
|
||||
expect(this.eventCardContainer.state.event.end).toBe(oldEnd)
|
||||
expect(getPendingEvent()._end).toBe(oldEnd)
|
||||
})
|
||||
});
|
||||
|
||||
it("adjusts the times to prevent invalid times", () => {
|
||||
stubCalendars(testCalendars())
|
||||
setNewTestEvent(() => {
|
||||
const eventCard = this.eventCardContainer.refs.newEventCard;
|
||||
let event = this.eventCardContainer.state.event;
|
||||
|
||||
const start0 = now();
|
||||
const end0 = now().add(1, 'hour');
|
||||
|
||||
const start1 = now().add(2, 'hours');
|
||||
const expectedEnd1 = now().add(3, 'hours');
|
||||
|
||||
const expectedStart2 = now().subtract(3, 'hours');
|
||||
const end2 = now().subtract(2, 'hours');
|
||||
|
||||
// The event has the start times
|
||||
expect(event.start).toBe(start0.unix())
|
||||
expect(event.end).toBe(end0.unix())
|
||||
expect(getPendingEvent()._start).toBe(start0.unix())
|
||||
expect(getPendingEvent()._end).toBe(end0.unix())
|
||||
|
||||
eventCard._onChangeStartTime(start1.valueOf());
|
||||
|
||||
// The event the new start time and also moved the end to match
|
||||
event = this.eventCardContainer.state.event;
|
||||
expect(event.start).toBe(start1.unix())
|
||||
expect(event.end).toBe(expectedEnd1.unix())
|
||||
expect(getPendingEvent()._start).toBe(start1.unix())
|
||||
expect(getPendingEvent()._end).toBe(expectedEnd1.unix())
|
||||
|
||||
eventCard._onChangeEndTime(end2.valueOf());
|
||||
|
||||
// The event the new end time and also moved the start to match
|
||||
event = this.eventCardContainer.state.event;
|
||||
expect(event.start).toBe(expectedStart2.unix())
|
||||
expect(event.end).toBe(end2.unix())
|
||||
expect(getPendingEvent()._start).toBe(expectedStart2.unix())
|
||||
expect(getPendingEvent()._end).toBe(end2.unix())
|
||||
})
|
||||
});
|
||||
});
|
|
@ -0,0 +1,342 @@
|
|||
import _ from 'underscore'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ReactTestUtils from 'react-addons-test-utils'
|
||||
import ProposedTimePicker from '../lib/calendar/proposed-time-picker'
|
||||
import TestProposalDataSource from './test-proposal-data-source'
|
||||
import WeekView from '../../../src/components/nylas-calendar/week-view'
|
||||
import ProposedTimeCalendarStore from '../lib/proposed-time-calendar-store'
|
||||
|
||||
import {NylasCalendar} from 'nylas-component-kit'
|
||||
|
||||
import {activate, deactivate} from '../lib/main'
|
||||
|
||||
const now = window.testNowMoment
|
||||
|
||||
/**
|
||||
* This tests the ProposedTimePicker as an integration test of the picker,
|
||||
* associated calendar object, the ProposedTimeCalendarStore, and stubbed
|
||||
* ProposedTimeCalendarDataSource
|
||||
*
|
||||
*/
|
||||
describe("ProposedTimePicker", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(NylasEnv, "getWindowType").andReturn("calendar");
|
||||
spyOn(WeekView.prototype, "_now").andReturn(now());
|
||||
spyOn(NylasCalendar.prototype, "_now").andReturn(now());
|
||||
activate()
|
||||
|
||||
this.testSrc = new TestProposalDataSource()
|
||||
spyOn(ProposedTimePicker.prototype, "_dataSource").andReturn(this.testSrc)
|
||||
this.picker = ReactTestUtils.renderIntoDocument(
|
||||
<ProposedTimePicker />
|
||||
)
|
||||
this.weekView = ReactTestUtils.findRenderedComponentWithType(this.picker, WeekView);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
deactivate()
|
||||
})
|
||||
|
||||
it("renders a proposed time picker in week view", () => {
|
||||
const picker = ReactTestUtils.findRenderedComponentWithType(this.picker, ProposedTimePicker);
|
||||
const weekView = ReactTestUtils.findRenderedComponentWithType(this.picker, WeekView);
|
||||
expect(picker instanceof ProposedTimePicker).toBe(true);
|
||||
expect(weekView instanceof WeekView).toBe(true);
|
||||
});
|
||||
|
||||
// NOTE: We manually fire the SchedulerActions since we've tested the
|
||||
// mouse click to time conversion in the nylas-calendar
|
||||
|
||||
it("creates a proposal on click", () => {
|
||||
this.picker._onCalendarMouseDown({
|
||||
time: now(),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
this.picker._onCalendarMouseUp({
|
||||
time: now(),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);
|
||||
expect(ProposedTimeCalendarStore.proposals().length).toBe(1)
|
||||
expect(ProposedTimeCalendarStore.proposalsAsEvents().length).toBe(1)
|
||||
const proposals = $("proposal");
|
||||
const events = $("calendar-event");
|
||||
expect(events.length).toBe(1);
|
||||
expect(proposals.length).toBe(1);
|
||||
|
||||
// It's not an availability block but a full blown proposal
|
||||
expect($("availability").length).toBe(0);
|
||||
});
|
||||
|
||||
it("creates the time picker for the correct timespan", () => {
|
||||
const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);
|
||||
const title = $("title");
|
||||
expect(ReactDOM.findDOMNode(title[0]).innerHTML).toBe("March 13 - March 19 2016");
|
||||
});
|
||||
|
||||
it("creates a block of proposals on drag down", () => {
|
||||
this.picker._onCalendarMouseDown({
|
||||
time: now(),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
this.picker._onCalendarMouseMove({
|
||||
time: now().add(30, 'minutes'),
|
||||
mouseIsDown: true,
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
this.picker._onCalendarMouseMove({
|
||||
time: now().add(60, 'minutes'),
|
||||
mouseIsDown: true,
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);
|
||||
|
||||
// Ensure that we don't see any proposals
|
||||
expect(ProposedTimeCalendarStore.proposals().length).toBe(0)
|
||||
let proposalEls = $("proposal");
|
||||
expect(proposalEls.length).toBe(0);
|
||||
|
||||
// But we DO see the drag block event
|
||||
expect(ProposedTimeCalendarStore.proposalsAsEvents().length).toBe(1)
|
||||
let events = $("calendar-event");
|
||||
expect($("availability").length).toBe(1);
|
||||
expect(events.length).toBe(1);
|
||||
|
||||
this.picker._onCalendarMouseUp({
|
||||
time: now().add(90, 'minutes'),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
|
||||
// Now that we've moused up, this should convert them into proposals
|
||||
const proposals = ProposedTimeCalendarStore.proposals()
|
||||
expect(proposals.length).toBe(3)
|
||||
expect(ProposedTimeCalendarStore.proposalsAsEvents().length).toBe(3)
|
||||
proposalEls = $("proposal");
|
||||
events = $("calendar-event");
|
||||
expect(events.length).toBe(3);
|
||||
expect(proposalEls.length).toBe(3);
|
||||
|
||||
const times = proposals.map((p) =>
|
||||
[p.start, p.end]
|
||||
);
|
||||
|
||||
expect(times).toEqual([
|
||||
[now().unix(), now().add(30, 'minutes').unix() - 1],
|
||||
[now().add(30, 'minutes').unix(),
|
||||
now().add(60, 'minutes').unix() - 1],
|
||||
[now().add(60, 'minutes').unix(),
|
||||
now().add(90, 'minutes').unix() - 1],
|
||||
]);
|
||||
});
|
||||
|
||||
it("creates a block of proposals on drag up", () => {
|
||||
this.picker._onCalendarMouseDown({
|
||||
time: now(),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
this.picker._onCalendarMouseMove({
|
||||
time: now().subtract(30, 'minutes'),
|
||||
mouseIsDown: true,
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
this.picker._onCalendarMouseMove({
|
||||
time: now().subtract(60, 'minutes'),
|
||||
mouseIsDown: true,
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
this.picker._onCalendarMouseUp({
|
||||
time: now().subtract(90, 'minutes'),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
|
||||
const proposals = ProposedTimeCalendarStore.proposals()
|
||||
const times = proposals.map((p) =>
|
||||
[p.start, p.end]
|
||||
);
|
||||
|
||||
expect(times).toEqual([
|
||||
[now().subtract(90, 'minutes').unix(),
|
||||
now().subtract(60, 'minutes').unix() - 1],
|
||||
[now().subtract(60, 'minutes').unix(),
|
||||
now().subtract(30, 'minutes').unix() - 1],
|
||||
[now().subtract(30, 'minutes').unix(),
|
||||
now().unix() - 1],
|
||||
]);
|
||||
});
|
||||
|
||||
it("removes proposals when clicked on", () => {
|
||||
// This created a proposal
|
||||
this.picker._onCalendarMouseDown({
|
||||
time: now(),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
this.picker._onCalendarMouseUp({
|
||||
time: now(),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
|
||||
// See the proposal is there
|
||||
expect(ProposedTimeCalendarStore.proposals().length).toBe(1)
|
||||
|
||||
// Now let's find and click it.
|
||||
// This also tests to make sure it actually rendered
|
||||
const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);
|
||||
const removeBtn = $("rm-time proposal");
|
||||
expect(removeBtn.length).toBe(1)
|
||||
ReactTestUtils.Simulate.mouseDown(ReactDOM.findDOMNode(removeBtn[0]))
|
||||
|
||||
// Now see that it's gone!
|
||||
expect(ProposedTimeCalendarStore.proposals().length).toBe(0)
|
||||
// And gone from the DOM too.
|
||||
expect($("proposal").length).toBe(0);
|
||||
// And didn't turn into an availability block or something dumb
|
||||
expect($("availability").length).toBe(0);
|
||||
});
|
||||
|
||||
it("can clear all of the proposals", () => {
|
||||
// This created a proposal
|
||||
this.picker._onCalendarMouseDown({
|
||||
time: now(),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
this.picker._onCalendarMouseUp({
|
||||
time: now(),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
|
||||
// See the proposal is there
|
||||
expect(ProposedTimeCalendarStore.proposals().length).toBe(1)
|
||||
|
||||
// Find the clear button
|
||||
const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);
|
||||
const clearBtns = $("clear-proposed-times");
|
||||
expect(clearBtns.length).toBe(1);
|
||||
|
||||
// Click it
|
||||
ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(clearBtns[0]))
|
||||
|
||||
// Ensure no more proposals
|
||||
expect(ProposedTimeCalendarStore.proposals().length).toBe(0)
|
||||
// And nothing still rendered
|
||||
expect($("proposal").length).toBe(0);
|
||||
expect($("availability").length).toBe(0);
|
||||
});
|
||||
|
||||
it("can change the duration", () => {
|
||||
// Find the duration picker.
|
||||
const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);
|
||||
const pickerEls = $("duration-picker-select");
|
||||
expect(pickerEls.length).toBe(1);
|
||||
|
||||
// Starts with default duration
|
||||
const d30Min = ProposedTimeCalendarStore.DURATIONS[0]
|
||||
expect(ProposedTimeCalendarStore._duration).toEqual(d30Min)
|
||||
|
||||
const pickerEl = ReactDOM.findDOMNode(pickerEls[0]);
|
||||
pickerEl.value = "1.5|hours|1½ hr"
|
||||
ReactTestUtils.Simulate.change(pickerEl)
|
||||
|
||||
const dHrHalf = ProposedTimeCalendarStore.DURATIONS[2]
|
||||
dHrHalf[0] = `${dHrHalf[0]}` // convert to string
|
||||
expect(ProposedTimeCalendarStore._duration).toEqual(dHrHalf)
|
||||
});
|
||||
|
||||
it("creates a block of proposals with a longer duration", () => {
|
||||
const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);
|
||||
|
||||
// Create a single proposal with the default 30 min duration.
|
||||
this.picker._onCalendarMouseDown({
|
||||
time: now(),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
this.picker._onCalendarMouseUp({
|
||||
time: now(),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
|
||||
// It's 30 min long
|
||||
const proposals = ProposedTimeCalendarStore.proposals()
|
||||
const times = proposals.map((p) => [p.start, p.end]);
|
||||
expect(times).toEqual([
|
||||
[now().unix(),
|
||||
now().add(30, 'minutes').unix() - 1],
|
||||
]);
|
||||
|
||||
// Change duration to 2.5 hours
|
||||
const pickerEl = ReactDOM.findDOMNode($("duration-picker-select")[0]);
|
||||
pickerEl.value = "2.5|hours|2½ hr"
|
||||
ReactTestUtils.Simulate.change(pickerEl)
|
||||
|
||||
// Click a new event
|
||||
this.picker._onCalendarMouseDown({
|
||||
time: now().add(2, 'hours'),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
this.picker._onCalendarMouseUp({
|
||||
time: now().add(2, 'hours'),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
|
||||
// It should have added a 2.5 hour long event and left the original
|
||||
// event alone
|
||||
const p2 = ProposedTimeCalendarStore.proposals()
|
||||
const t2 = p2.map((p) => [p.start, p.end]);
|
||||
expect(t2).toEqual([
|
||||
[now().unix(),
|
||||
now().add(30, 'minutes').unix() - 1],
|
||||
[now().add(2, 'hours').unix(),
|
||||
now().add(4.5, 'hours').unix() - 1],
|
||||
]);
|
||||
});
|
||||
|
||||
it("overrides events so they don't overlap", () => {
|
||||
const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);
|
||||
this.picker._onCalendarMouseDown({
|
||||
time: now(),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
this.picker._onCalendarMouseUp({
|
||||
time: now().add(1, 'hour'),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
|
||||
// Creates two proposals.
|
||||
const proposals = ProposedTimeCalendarStore.proposals()
|
||||
const times = proposals.map((p) => [p.start, p.end]);
|
||||
expect(times).toEqual([
|
||||
[now().unix(),
|
||||
now().add(30, 'minutes').unix() - 1],
|
||||
[now().add(30, 'minutes').unix(),
|
||||
now().add(60, 'minutes').unix() - 1],
|
||||
]);
|
||||
|
||||
// Change the duration to 2 hours
|
||||
const pickerEl = ReactDOM.findDOMNode($("duration-picker-select")[0]);
|
||||
pickerEl.value = "2|hours|2 hr"
|
||||
ReactTestUtils.Simulate.change(pickerEl)
|
||||
|
||||
// Click and drag overlapping the first of the original events.
|
||||
this.picker._onCalendarMouseDown({
|
||||
time: now().subtract(1.5, 'hours'),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
this.picker._onCalendarMouseUp({
|
||||
time: now().add(20, 'minutes'),
|
||||
currentView: NylasCalendar.WEEK_VIEW,
|
||||
})
|
||||
|
||||
// See that there's only 1 new event with the correct time and it
|
||||
// exhchanged it with the old one.
|
||||
//
|
||||
// It left the non overlapping one alone.
|
||||
const p2 = ProposedTimeCalendarStore.proposals()
|
||||
const t2 = p2.map((p) => [p.start, p.end]);
|
||||
expect(t2).toEqual([
|
||||
[now().add(30, 'minutes').unix(),
|
||||
now().add(60, 'minutes').unix() - 1],
|
||||
[now().subtract(1.5, 'hours').unix(),
|
||||
now().add(30, 'minutes').unix() - 1],
|
||||
]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,184 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ReactTestUtils from 'react-addons-test-utils'
|
||||
|
||||
import {DatabaseStore, Calendar, APIError, Actions, NylasAPI} from 'nylas-exports'
|
||||
import {PLUGIN_ID, PLUGIN_NAME} from '../lib/scheduler-constants'
|
||||
|
||||
import SchedulerComposerButton from '../lib/composer/scheduler-composer-button'
|
||||
|
||||
import NewEventHelper from '../lib/composer/new-event-helper'
|
||||
|
||||
import {
|
||||
prepareDraft,
|
||||
cleanupDraft,
|
||||
setupCalendars,
|
||||
DRAFT_CLIENT_ID,
|
||||
} from './composer-scheduler-spec-helper'
|
||||
|
||||
const now = window.testNowMoment;
|
||||
|
||||
describe("SchedulerComposerButton", () => {
|
||||
beforeEach(() => {
|
||||
this.session = null
|
||||
spyOn(Actions, "openPopover").andCallThrough();
|
||||
spyOn(Actions, "closePopover").andCallThrough();
|
||||
spyOn(NylasEnv, "reportError")
|
||||
spyOn(NylasEnv, "showErrorDialog")
|
||||
spyOn(NewEventHelper, "now").andReturn(now())
|
||||
// Will eventually fill this.session
|
||||
prepareDraft.call(this)
|
||||
|
||||
// Note: Needs to be in a `runs` block so it happens after the async
|
||||
// activities of `prepareDraft`
|
||||
runs(() => {
|
||||
this.schedulerBtn = ReactTestUtils.renderIntoDocument(
|
||||
<SchedulerComposerButton draftClientId={DRAFT_CLIENT_ID} />
|
||||
);
|
||||
})
|
||||
|
||||
waitsFor(() => this.schedulerBtn._session)
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupDraft()
|
||||
})
|
||||
|
||||
const spyAuthSuccess = () => {
|
||||
spyOn(NylasAPI, "authPlugin").andCallFake((pluginId, pluginName, accountId) => {
|
||||
expect(pluginId).toBe(PLUGIN_ID);
|
||||
expect(pluginName).toBe(PLUGIN_NAME);
|
||||
expect(accountId).toBe(window.TEST_ACCOUNT_ID);
|
||||
return Promise.resolve();
|
||||
})
|
||||
}
|
||||
|
||||
it("loads the draft and renders the button", () => {
|
||||
const el = ReactTestUtils.findRenderedComponentWithType(this.schedulerBtn,
|
||||
SchedulerComposerButton);
|
||||
expect(el instanceof SchedulerComposerButton).toBe(true)
|
||||
});
|
||||
|
||||
const testForError = () => {
|
||||
runs(() => {
|
||||
ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(this.schedulerBtn));
|
||||
})
|
||||
waitsFor(() =>
|
||||
NylasEnv.showErrorDialog.calls.length > 0
|
||||
);
|
||||
runs(() => {
|
||||
const picker = document.querySelector(".scheduler-picker")
|
||||
expect(Actions.openPopover).toHaveBeenCalled();
|
||||
expect(Actions.closePopover).toHaveBeenCalled();
|
||||
expect(picker).toBe(null);
|
||||
})
|
||||
}
|
||||
|
||||
it("errors on 400 error and reports", () => {
|
||||
const err = new APIError({statusCode: 400});
|
||||
spyOn(NylasAPI, "authPlugin").andReturn(Promise.reject(err));
|
||||
testForError(err);
|
||||
runs(() => {
|
||||
expect(NylasEnv.reportError).toHaveBeenCalledWith(err);
|
||||
})
|
||||
});
|
||||
|
||||
it("errors on unexpected errors and reports", () => {
|
||||
const err = new Error("OH NO");
|
||||
spyOn(NylasAPI, "authPlugin").andReturn(Promise.reject(err));
|
||||
testForError(err);
|
||||
runs(() => {
|
||||
expect(NylasEnv.reportError).toHaveBeenCalledWith(err);
|
||||
})
|
||||
});
|
||||
|
||||
it("errors on offline, but doesn't report", () => {
|
||||
const err = new APIError({statusCode: 0});
|
||||
spyOn(NylasAPI, "authPlugin").andReturn(Promise.reject(err));
|
||||
testForError(err);
|
||||
runs(() => {
|
||||
expect(NylasEnv.reportError).not.toHaveBeenCalled();
|
||||
})
|
||||
});
|
||||
|
||||
describe("auth success", () => {
|
||||
beforeEach(() => {
|
||||
spyAuthSuccess();
|
||||
ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(this.schedulerBtn));
|
||||
const items = document.querySelectorAll(".scheduler-picker .item");
|
||||
this.meetingRequestBtn = items[0];
|
||||
this.proposalBtn = items[1];
|
||||
});
|
||||
|
||||
it("renders the popover on click", () => {
|
||||
// The popover renders outside the scope of the component.
|
||||
const picker = document.querySelector(".scheduler-picker")
|
||||
expect(Actions.openPopover).toHaveBeenCalled();
|
||||
expect(picker).toBeDefined();
|
||||
});
|
||||
|
||||
it("auths the plugin on click", () => {
|
||||
expect(NylasAPI.authPlugin).toHaveBeenCalled()
|
||||
expect(NylasAPI.authPlugin.calls.length).toBe(1)
|
||||
});
|
||||
|
||||
it("creates a new event on the metadata", () => {
|
||||
setupCalendars();
|
||||
spyOn(this.session.changes, "addPluginMetadata").andCallThrough()
|
||||
runs(() => {
|
||||
ReactTestUtils.Simulate.mouseDown(this.meetingRequestBtn);
|
||||
})
|
||||
waitsFor(() =>
|
||||
this.session.changes.addPluginMetadata.calls.length > 0
|
||||
);
|
||||
runs(() => {
|
||||
expect(Actions.closePopover).toHaveBeenCalled();
|
||||
const metadata = this.session.draft().metadataForPluginId(PLUGIN_ID);
|
||||
expect(metadata.pendingEvent._start).toBe(now().unix())
|
||||
expect(metadata.pendingEvent._end).toBe(now().add(1, 'hour').unix())
|
||||
expect(NylasEnv.showErrorDialog).not.toHaveBeenCalled()
|
||||
})
|
||||
});
|
||||
|
||||
// NOTE: The backend requires a `uid` key on the metadata in order to
|
||||
// properly look up the pending event. This must be present in order
|
||||
// for the service to work.
|
||||
it("IMPORTANT: puts the draft client ID on the `uid` key", () => {
|
||||
setupCalendars();
|
||||
spyOn(this.session.changes, "addPluginMetadata").andCallThrough()
|
||||
runs(() => {
|
||||
ReactTestUtils.Simulate.mouseDown(this.meetingRequestBtn);
|
||||
})
|
||||
waitsFor(() =>
|
||||
this.session.changes.addPluginMetadata.calls.length > 0
|
||||
);
|
||||
runs(() => {
|
||||
const metadata = this.session.draft().metadataForPluginId(PLUGIN_ID);
|
||||
expect(metadata.uid).toBe(DRAFT_CLIENT_ID)
|
||||
})
|
||||
});
|
||||
|
||||
it("throws an error if there aren't any calendars", () => {
|
||||
// Only a read-only calendar
|
||||
spyOn(DatabaseStore, "findAll").andCallFake((klass, {accountId}) => {
|
||||
const cals = [
|
||||
new Calendar({accountId, readOnly: true, name: 'b'}),
|
||||
]
|
||||
return Promise.resolve(cals);
|
||||
})
|
||||
|
||||
runs(() => {
|
||||
ReactTestUtils.Simulate.mouseDown(this.meetingRequestBtn);
|
||||
})
|
||||
waitsFor(() =>
|
||||
NylasEnv.showErrorDialog.calls.length > 0
|
||||
);
|
||||
runs(() => {
|
||||
expect(Actions.closePopover).toHaveBeenCalled();
|
||||
const metadata = this.session.draft().metadataForPluginId(PLUGIN_ID);
|
||||
expect(metadata).toBe(null);
|
||||
expect(NylasEnv.showErrorDialog.calls.length).toBe(1)
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,148 @@
|
|||
import {PLUGIN_ID} from '../lib/scheduler-constants'
|
||||
import {
|
||||
prepareDraft,
|
||||
setupCalendars,
|
||||
cleanupDraft,
|
||||
DRAFT_CLIENT_ID,
|
||||
} from './composer-scheduler-spec-helper'
|
||||
import NewEventHelper from '../lib/composer/new-event-helper'
|
||||
import SchedulerComposerExtension from '../lib/composer/scheduler-composer-extension'
|
||||
|
||||
import {DatabaseStore} from 'nylas-exports';
|
||||
|
||||
import Proposal from '../lib/proposal'
|
||||
import SchedulerActions from '../lib/scheduler-actions'
|
||||
|
||||
const now = window.testNowMoment;
|
||||
|
||||
describe("SchedulerComposerExtension", () => {
|
||||
beforeEach(() => {
|
||||
this.session = null
|
||||
// Will eventually fill this.session
|
||||
prepareDraft.call(this);
|
||||
setupCalendars.call(this);
|
||||
spyOn(NewEventHelper, "now").andReturn(now())
|
||||
|
||||
// Note: Needs to be in a `runs` block so it happens after the async
|
||||
// activities of `prepareDraft`
|
||||
runs(() => {
|
||||
NewEventHelper.addEventToSession(this.session)
|
||||
})
|
||||
waitsFor(() =>
|
||||
this.session.draft().metadataForPluginId(PLUGIN_ID)
|
||||
)
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanupDraft()
|
||||
})
|
||||
|
||||
describe("Inserting a new event", () => {
|
||||
beforeEach(() => {
|
||||
this.nextDraft = SchedulerComposerExtension.applyTransformsToDraft({
|
||||
draft: this.session.draft(),
|
||||
});
|
||||
});
|
||||
|
||||
it("Inserts the proposted-time-list", () => {
|
||||
expect(this.nextDraft.body).toMatch(/new-event-preview/);
|
||||
});
|
||||
|
||||
it("Has the correct start and end times in the body", () => {
|
||||
expect(this.nextDraft.body).toMatch(/Tuesday, March 15, 2016 <br\/>12:00 PM – 1:00 PM/);
|
||||
});
|
||||
|
||||
it("Doesn't include proposed times", () => {
|
||||
expect(this.nextDraft.body).not.toMatch(/proposed-time-table/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Inserting proposed times", () => {
|
||||
beforeEach(() => {
|
||||
const draft = this.session.draft()
|
||||
spyOn(DatabaseStore, "find").andReturn(Promise.resolve(draft));
|
||||
const start = now().add(1, 'hour').unix();
|
||||
const end = now().add(2, 'hours').unix();
|
||||
this.proposals = [new Proposal({start, end})]
|
||||
|
||||
runs(() => {
|
||||
SchedulerActions.confirmChoices({
|
||||
proposals: this.proposals,
|
||||
draftClientId: DRAFT_CLIENT_ID,
|
||||
});
|
||||
})
|
||||
waitsFor(() => {
|
||||
const metadata = this.session.draft().metadataForPluginId(PLUGIN_ID);
|
||||
return (metadata.proposals || []).length > 0;
|
||||
})
|
||||
});
|
||||
|
||||
it("inserts proposed times on metadata", () => {
|
||||
const metadata = this.session.draft().metadataForPluginId(PLUGIN_ID);
|
||||
expect(metadata.proposals).toBe(this.proposals);
|
||||
});
|
||||
|
||||
it("inserts the proposals into the draft body", () => {
|
||||
const nextDraft = SchedulerComposerExtension.applyTransformsToDraft({
|
||||
draft: this.session.draft(),
|
||||
});
|
||||
expect(nextDraft.body).not.toMatch(/new-event-preview/);
|
||||
expect(nextDraft.body).toMatch(/proposed-time-table/);
|
||||
expect(nextDraft.body).toMatch(/1:00 PM — 2:00 PM/);
|
||||
});
|
||||
});
|
||||
|
||||
// The backend will use whatever is stored in the `pendingEvent` field
|
||||
// to POST to the /events API endpoint. This means the data must be
|
||||
// a valid event. Verify that it meets Nylas API specs
|
||||
describe("When setting the event JSON to match server requirements", () => {
|
||||
beforeEach(() => {
|
||||
SchedulerComposerExtension.applyTransformsToDraft({
|
||||
draft: this.session.draft(),
|
||||
});
|
||||
const metadata = this.session.draft().metadataForPluginId(PLUGIN_ID);
|
||||
this.pendingEvent = metadata.pendingEvent
|
||||
});
|
||||
|
||||
it("doesn't have a clientId", () => {
|
||||
expect(this.pendingEvent.client_id).not.toBeDefined();
|
||||
expect(this.pendingEvent.clientId).not.toBeDefined();
|
||||
});
|
||||
|
||||
it("doesn't have an id", () => {
|
||||
expect(this.pendingEvent.id).not.toBeDefined();
|
||||
expect(this.pendingEvent.serverId).not.toBeDefined();
|
||||
expect(this.pendingEvent.server_id).not.toBeDefined();
|
||||
});
|
||||
|
||||
it("has the correct `when` block", () => {
|
||||
expect(this.pendingEvent.when).toEqual({
|
||||
start_time: now().unix(),
|
||||
end_time: now().add(1, 'hour').unix(),
|
||||
})
|
||||
expect(this.pendingEvent.when.object).not.toBeDefined();
|
||||
});
|
||||
|
||||
it("doesn't have _start or _end blocks", () => {
|
||||
expect(this.pendingEvent._start).not.toBeDefined();
|
||||
expect(this.pendingEvent._end).not.toBeDefined();
|
||||
});
|
||||
|
||||
it("has the correct participants", () => {
|
||||
const from = this.session.draft().from[0]
|
||||
expect(this.pendingEvent.participants.length).toBe(1);
|
||||
expect(this.pendingEvent.participants[0].name).toBe(from.name);
|
||||
expect(this.pendingEvent.participants[0].email).toBe(from.email);
|
||||
expect(this.pendingEvent.participants[0].status).toBe("noreply");
|
||||
});
|
||||
|
||||
it("only has appropriate keys", () => {
|
||||
expect(Object.keys(this.pendingEvent)).toEqual([
|
||||
"calendar_id",
|
||||
"title",
|
||||
"participants",
|
||||
"when",
|
||||
])
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
import ProposedTimeCalendarStore from '../lib/proposed-time-calendar-store'
|
||||
import {CalendarDataSource} from 'nylas-exports'
|
||||
|
||||
export default class TestProposalDataSource extends CalendarDataSource {
|
||||
buildObservable({startTime, endTime}) {
|
||||
this.endTime = endTime
|
||||
this.startTime = startTime
|
||||
this._usub = ProposedTimeCalendarStore.listen(this.manuallyTrigger)
|
||||
return this
|
||||
}
|
||||
|
||||
manuallyTrigger = () => {
|
||||
this.onNext({events: ProposedTimeCalendarStore.proposalsAsEvents()})
|
||||
}
|
||||
|
||||
subscribe(onNext) {
|
||||
this.onNext = onNext
|
||||
this.manuallyTrigger()
|
||||
const dispose = () => {
|
||||
this._usub()
|
||||
}
|
||||
return {dispose}
|
||||
}
|
||||
}
|
132
spec/components/nylas-calendar/fixtures/events.es6
Normal file
132
spec/components/nylas-calendar/fixtures/events.es6
Normal file
|
@ -0,0 +1,132 @@
|
|||
import moment from 'moment-timezone'
|
||||
import {Event} from 'nylas-exports'
|
||||
import {TZ, TEST_CALENDAR} from '../test-utils'
|
||||
|
||||
// All day
|
||||
// All day overlap
|
||||
//
|
||||
// Simple single event
|
||||
// Event that spans a day
|
||||
// Overlapping events
|
||||
|
||||
let gen = 0
|
||||
|
||||
const genEvent = ({start, end, object = "timespan"}) => {
|
||||
gen += 1;
|
||||
|
||||
let when = {}
|
||||
if (object === "timespan") {
|
||||
when = {
|
||||
object: "timespan",
|
||||
end_time: moment.tz(end, TZ).unix(),
|
||||
start_time: moment.tz(start, TZ).unix(),
|
||||
}
|
||||
}
|
||||
if (object === "datespan") {
|
||||
when = {
|
||||
object: "datespan",
|
||||
end_date: end,
|
||||
start_date: start,
|
||||
}
|
||||
}
|
||||
|
||||
return new Event().fromJSON({
|
||||
id: `server-${gen}`,
|
||||
calendar_id: TEST_CALENDAR,
|
||||
account_id: window.TEST_ACCOUNT_ID,
|
||||
description: `description ${gen}`,
|
||||
location: `location ${gen}`,
|
||||
owner: `${window._TEST_ACCOUNT_NAME} <${window.TEST_ACCOUNT_EMAIL}>`,
|
||||
participants: [{
|
||||
email: window.TEST_ACCOUNT_EMAIL,
|
||||
name: window.TEST_ACCOUNT_NAME,
|
||||
status: "yes",
|
||||
}],
|
||||
read_only: "false",
|
||||
title: `Title ${gen}`,
|
||||
busy: true,
|
||||
when,
|
||||
status: "confirmed",
|
||||
})
|
||||
}
|
||||
|
||||
// NOTE:
|
||||
// DST Started 2016-03-13 01:59 and immediately jumps to 03:00.
|
||||
// DST Ended 2016-11-06 01:59 and immediately jumps to 01:00 again!
|
||||
//
|
||||
// See: http://momentjs.com/timezone/docs/#/using-timezones/parsing-ambiguous-inputs/
|
||||
|
||||
// All times are in "America/Los_Angeles"
|
||||
export const numAllDayEvents = 6
|
||||
export const numStandardEvents = 9
|
||||
export const numByDay = {
|
||||
1457769600: 2,
|
||||
1457856000: 7,
|
||||
}
|
||||
export const eventOverlapForSunday = {
|
||||
"server-2": {
|
||||
concurrentEvents: 2,
|
||||
order: 1,
|
||||
},
|
||||
"server-3": {
|
||||
concurrentEvents: 2,
|
||||
order: 2,
|
||||
},
|
||||
"server-6": {
|
||||
concurrentEvents: 1,
|
||||
order: 1,
|
||||
},
|
||||
"server-7": {
|
||||
concurrentEvents: 1,
|
||||
order: 1,
|
||||
},
|
||||
"server-8": {
|
||||
concurrentEvents: 2,
|
||||
order: 1,
|
||||
},
|
||||
"server-9": {
|
||||
concurrentEvents: 2,
|
||||
order: 2,
|
||||
},
|
||||
"server-10": {
|
||||
concurrentEvents: 2,
|
||||
order: 1,
|
||||
},
|
||||
}
|
||||
export const events = [
|
||||
// Single event
|
||||
genEvent({start: "2016-03-12 12:00", end: "2016-03-12 13:00"}),
|
||||
|
||||
// DST start spanning event. 6 hours when it should be 7!
|
||||
genEvent({start: "2016-03-12 23:00", end: "2016-03-13 06:00"}),
|
||||
|
||||
// DST start invalid event. Does not exist!
|
||||
genEvent({start: "2016-03-13 02:15", end: "2016-03-13 02:45"}),
|
||||
|
||||
// DST end spanning event. 8 hours when it shoudl be 7!
|
||||
genEvent({start: "2016-11-05 23:00", end: "2016-11-06 06:00"}),
|
||||
|
||||
// DST end ambiguous event. This timespan happens twice!
|
||||
genEvent({start: "2016-11-06 01:15", end: "2016-11-06 01:45"}),
|
||||
|
||||
// Adjacent events
|
||||
genEvent({start: "2016-03-13 12:00", end: "2016-03-13 13:00"}),
|
||||
genEvent({start: "2016-03-13 13:00", end: "2016-03-13 14:00"}),
|
||||
|
||||
// Overlapping events
|
||||
genEvent({start: "2016-03-13 14:30", end: "2016-03-13 15:30"}),
|
||||
genEvent({start: "2016-03-13 15:00", end: "2016-03-13 16:00"}),
|
||||
genEvent({start: "2016-03-13 15:30", end: "2016-03-13 16:30"}),
|
||||
|
||||
// All day timespan event
|
||||
genEvent({start: "2016-03-15 00:00", end: "2016-03-16 00:00"}),
|
||||
|
||||
// All day datespan
|
||||
genEvent({start: "2016-03-17", end: "2016-03-18", object: "datespan"}),
|
||||
|
||||
// Overlapping all day
|
||||
genEvent({start: "2016-03-19", end: "2016-03-20", object: "datespan"}),
|
||||
genEvent({start: "2016-03-19 00:00", end: "2016-03-20 00:00"}),
|
||||
genEvent({start: "2016-03-19 12:00", end: "2016-03-20 12:00"}),
|
||||
genEvent({start: "2016-03-20 00:00", end: "2016-03-21 00:00"}),
|
||||
]
|
17
spec/components/nylas-calendar/test-data-source.es6
Normal file
17
spec/components/nylas-calendar/test-data-source.es6
Normal file
|
@ -0,0 +1,17 @@
|
|||
// import Rx from 'rx-lite-testing'
|
||||
import {events} from './fixtures/events'
|
||||
import {CalendarDataSource} from 'nylas-exports'
|
||||
|
||||
export default class TestDataSource extends CalendarDataSource {
|
||||
buildObservable({startTime, endTime}) {
|
||||
this.endTime = endTime;
|
||||
this.startTime = startTime;
|
||||
return this
|
||||
}
|
||||
|
||||
subscribe(onNext) {
|
||||
onNext({events})
|
||||
this.unsubscribe = jasmine.createSpy("unusbscribe");
|
||||
return {dispose: this.unsubscribe}
|
||||
}
|
||||
}
|
14
spec/components/nylas-calendar/test-utils.es6
Normal file
14
spec/components/nylas-calendar/test-utils.es6
Normal file
|
@ -0,0 +1,14 @@
|
|||
import moment from 'moment-timezone'
|
||||
|
||||
export const TZ = window.TEST_TIME_ZONE;
|
||||
export const TEST_CALENDAR = "TEST_CALENDAR";
|
||||
|
||||
export const now = () => window.testNowMoment();
|
||||
|
||||
export const NOW_WEEK_START = moment.tz("2016-03-13 00:00", TZ);
|
||||
export const NOW_BUFFER_START = moment.tz("2016-03-06 00:00", TZ);
|
||||
export const NOW_BUFFER_END = moment.tz("2016-03-26 23:59:59", TZ);
|
||||
|
||||
// Makes test failure output easier to read.
|
||||
export const u2h = (unixTime) => moment.unix(unixTime).format("LLL z")
|
||||
export const m2h = (m) => m.format("LLL z")
|
|
@ -0,0 +1,5 @@
|
|||
import {events} from './fixtures/events'
|
||||
import {NylasCalendar} from 'nylas-component-kit'
|
||||
|
||||
describe("Extended Nylas Calendar Week View", () => {
|
||||
});
|
188
spec/components/nylas-calendar/week-view-spec.jsx
Normal file
188
spec/components/nylas-calendar/week-view-spec.jsx
Normal file
|
@ -0,0 +1,188 @@
|
|||
import _ from 'underscore'
|
||||
import moment from 'moment'
|
||||
import React from 'react'
|
||||
import ReactTestUtils from 'react-addons-test-utils'
|
||||
import {
|
||||
now,
|
||||
NOW_WEEK_START,
|
||||
NOW_BUFFER_START,
|
||||
NOW_BUFFER_END,
|
||||
} from './test-utils'
|
||||
import TestDataSource from './test-data-source'
|
||||
import {NylasCalendar} from 'nylas-component-kit'
|
||||
import {
|
||||
numByDay,
|
||||
numAllDayEvents,
|
||||
numStandardEvents,
|
||||
eventOverlapForSunday,
|
||||
} from './fixtures/events'
|
||||
|
||||
import WeekView from '../../../src/components/nylas-calendar/week-view'
|
||||
|
||||
describe("Nylas Calendar Week View", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(WeekView.prototype, "_now").andReturn(now());
|
||||
|
||||
this.onCalendarMouseDown = jasmine.createSpy("onCalendarMouseDown")
|
||||
this.dataSource = new TestDataSource();
|
||||
this.calendar = ReactTestUtils.renderIntoDocument(
|
||||
<NylasCalendar
|
||||
currentMoment={now()}
|
||||
onCalendarMouseDown={this.onCalendarMouseDown}
|
||||
dataSource={this.dataSource}
|
||||
/>
|
||||
);
|
||||
this.weekView = ReactTestUtils.findRenderedComponentWithType(this.calendar, WeekView);
|
||||
});
|
||||
|
||||
it("renders a calendar", () => {
|
||||
const cal = ReactTestUtils.findRenderedComponentWithType(this.calendar, NylasCalendar)
|
||||
expect(cal instanceof NylasCalendar).toBe(true)
|
||||
});
|
||||
|
||||
it("sets the correct moment", () => {
|
||||
expect(this.calendar.state.currentMoment.valueOf()).toBe(now().valueOf())
|
||||
});
|
||||
|
||||
it("defaulted to WeekView", () => {
|
||||
expect(this.calendar.state.currentView).toBe("week");
|
||||
expect(this.weekView instanceof WeekView).toBe(true);
|
||||
});
|
||||
|
||||
it("initializes the component", () => {
|
||||
expect(this.weekView.todayYear).toBe(now().year());
|
||||
expect(this.weekView.todayDayOfYear).toBe(now().dayOfYear());
|
||||
});
|
||||
|
||||
it("initializes the data source & state with the correct times", () => {
|
||||
expect(this.dataSource.startTime).toBe(NOW_BUFFER_START.unix());
|
||||
expect(this.dataSource.endTime).toBe(NOW_BUFFER_END.unix());
|
||||
expect(this.weekView.state.startMoment.unix()).toBe(NOW_BUFFER_START.unix());
|
||||
expect(this.weekView.state.endMoment.unix()).toBe(NOW_BUFFER_END.unix());
|
||||
expect(this.weekView._scrollTime).toBe(NOW_WEEK_START.unix())
|
||||
});
|
||||
|
||||
it("has the correct days in buffer", () => {
|
||||
const days = this.weekView._daysInView();
|
||||
expect(days.length).toBe(21);
|
||||
expect(days[0].dayOfYear()).toBe(66)
|
||||
expect(days[days.length - 1].dayOfYear()).toBe(86)
|
||||
});
|
||||
|
||||
it("shows the correct current week", () => {
|
||||
expect(this.weekView._currentWeekText()).toBe("March 13 - March 19 2016")
|
||||
});
|
||||
|
||||
it("goes to next week on click", () => {
|
||||
const nextBtn = this.weekView.refs.headerControls.refs.onNextAction
|
||||
expect(this.weekView.state.startMoment.unix()).toBe(NOW_BUFFER_START.unix());
|
||||
expect(this.weekView._scrollTime).toBe(NOW_WEEK_START.unix())
|
||||
|
||||
ReactTestUtils.Simulate.click(nextBtn);
|
||||
|
||||
expect((this.weekView.state.startMoment).unix())
|
||||
.toBe(moment(NOW_BUFFER_START).add(1, 'week').unix());
|
||||
|
||||
expect(this.weekView._scrollTime)
|
||||
.toBe(moment(NOW_WEEK_START).add(1, 'week').unix());
|
||||
});
|
||||
|
||||
it("goes to the previous week on click", () => {
|
||||
const prevBtn = this.weekView.refs.headerControls.refs.onPreviousAction
|
||||
expect(this.weekView.state.startMoment.unix()).toBe(NOW_BUFFER_START.unix());
|
||||
expect(this.weekView._scrollTime).toBe(NOW_WEEK_START.unix())
|
||||
|
||||
ReactTestUtils.Simulate.click(prevBtn);
|
||||
|
||||
expect((this.weekView.state.startMoment).unix())
|
||||
.toBe(moment(NOW_BUFFER_START).subtract(1, 'week').unix());
|
||||
|
||||
expect(this.weekView._scrollTime)
|
||||
.toBe(moment(NOW_WEEK_START).subtract(1, 'week').unix());
|
||||
});
|
||||
|
||||
it("goes to 'today' when the 'today' btn is pressed", () => {
|
||||
const todayBtn = this.weekView.refs.todayBtn;
|
||||
const nextBtn = this.weekView.refs.headerControls.refs.onNextAction
|
||||
ReactTestUtils.Simulate.click(nextBtn);
|
||||
ReactTestUtils.Simulate.click(todayBtn)
|
||||
|
||||
expect(this.weekView.state.startMoment.unix()).toBe(NOW_BUFFER_START.unix());
|
||||
expect(this.weekView._scrollTime).toBe(NOW_WEEK_START.unix())
|
||||
});
|
||||
|
||||
it("sets the interval height properly", () => {
|
||||
expect(this.weekView.state.intervalHeight).toBe(21)
|
||||
});
|
||||
|
||||
it("properly segments the events by day", () => {
|
||||
const days = this.weekView._daysInView();
|
||||
const eventsByDay = this.weekView._eventsByDay(days);
|
||||
|
||||
// See fixtures/events
|
||||
expect(eventsByDay.allDay.length).toBe(numAllDayEvents);
|
||||
for (const day in numByDay) {
|
||||
if (numByDay.hasOwnProperty(day)) {
|
||||
expect(eventsByDay[day].length).toBe(numByDay[day])
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("correctly stacks all day events", () => {
|
||||
const height = this.weekView.refs.weekViewAllDayEvents.props.height;
|
||||
// This means it's 3-high
|
||||
expect(height).toBe(64);
|
||||
});
|
||||
|
||||
it("correctly sets up the event overlap for a day", () => {
|
||||
const days = this.weekView._daysInView();
|
||||
const eventsByDay = this.weekView._eventsByDay(days);
|
||||
const eventOverlap = this.weekView._eventOverlap(eventsByDay['1457856000']);
|
||||
expect(eventOverlap).toEqual(eventOverlapForSunday)
|
||||
});
|
||||
|
||||
it("renders the events onto the grid", () => {
|
||||
const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.weekView);
|
||||
|
||||
const events = $("calendar-event");
|
||||
const standardEvents = $("calendar-event vertical");
|
||||
const allDayEvents = $("calendar-event horizontal");
|
||||
|
||||
expect(events.length).toBe(numStandardEvents + numAllDayEvents)
|
||||
expect(standardEvents.length).toBe(numStandardEvents)
|
||||
expect(allDayEvents.length).toBe(numAllDayEvents)
|
||||
});
|
||||
|
||||
it("finds the correct data from mouse events", () => {
|
||||
const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.weekView);
|
||||
|
||||
const eventContainer = this.weekView.refs.calendarEventContainer;
|
||||
|
||||
// Unfortunately, _dataFromMouseEvent requires the component to both
|
||||
// be mounted and have size. To truly test this we'd have to load the
|
||||
// integratino test environment. For now, we test that the event makes
|
||||
// its way back to passed in callback handlers
|
||||
const mouseData = {
|
||||
x: 100,
|
||||
y: 100,
|
||||
width: 100,
|
||||
height: 100,
|
||||
time: now(),
|
||||
}
|
||||
spyOn(eventContainer, "_dataFromMouseEvent").andReturn(mouseData)
|
||||
|
||||
const eventEl = $("calendar-event vertical")[0];
|
||||
ReactTestUtils.Simulate.mouseDown(eventEl, {x: 100, y: 100});
|
||||
|
||||
const mouseEvent = eventContainer._dataFromMouseEvent.calls[0].args[0];
|
||||
expect(mouseEvent.x).toBe(100)
|
||||
expect(mouseEvent.y).toBe(100)
|
||||
|
||||
const mouseDataOut = this.onCalendarMouseDown.calls[0].args[0]
|
||||
expect(mouseDataOut.x).toEqual(mouseData.x)
|
||||
expect(mouseDataOut.y).toEqual(mouseData.y)
|
||||
expect(mouseDataOut.width).toEqual(mouseData.width)
|
||||
expect(mouseDataOut.height).toEqual(mouseData.height)
|
||||
expect(mouseDataOut.time.unix()).toEqual(mouseData.time.unix())
|
||||
});
|
||||
});
|
|
@ -114,6 +114,12 @@ window.TEST_ACCOUNT_NAME = "Nylas Test"
|
|||
window.TEST_PLUGIN_ID = "test-plugin-id-123"
|
||||
window.TEST_ACCOUNT_ALIAS_EMAIL = "tester+alternative@nylas.com"
|
||||
|
||||
window.TEST_TIME_ZONE = "America/Los_Angeles"
|
||||
moment = require('moment-timezone')
|
||||
# This date was chosen because it's close to a DST boundary
|
||||
window.testNowMoment = ->
|
||||
moment.tz("2016-03-15 12:00", TEST_TIME_ZONE)
|
||||
|
||||
beforeEach ->
|
||||
NylasEnv.testOrganizationUnit = null
|
||||
Grim.clearDeprecations() if isCoreSpec
|
||||
|
|
|
@ -24,7 +24,10 @@ export default class HeaderControls extends React.Component {
|
|||
_renderNextAction() {
|
||||
if (!this.props.nextAction) { return false; }
|
||||
return (
|
||||
<button className="btn btn-icon next" onClick={this.props.nextAction}>
|
||||
<button className="btn btn-icon next"
|
||||
ref="onNextAction"
|
||||
onClick={this.props.nextAction}
|
||||
>
|
||||
<RetinaImg name="ic-calendar-right-arrow.png"
|
||||
mode={RetinaImg.Mode.ContentIsMask}
|
||||
/>
|
||||
|
@ -35,7 +38,10 @@ export default class HeaderControls extends React.Component {
|
|||
_renderPrevAction() {
|
||||
if (!this.props.prevAction) { return false; }
|
||||
return (
|
||||
<button className="btn btn-icon prev" onClick={this.props.prevAction}>
|
||||
<button className="btn btn-icon prev"
|
||||
ref="onPreviousAction"
|
||||
onClick={this.props.prevAction}
|
||||
>
|
||||
<RetinaImg name="ic-calendar-left-arrow.png"
|
||||
mode={RetinaImg.Mode.ContentIsMask}
|
||||
/>
|
||||
|
|
|
@ -17,6 +17,8 @@ export default class NylasCalendar extends React.Component {
|
|||
*/
|
||||
dataSource: React.PropTypes.instanceOf(CalendarDataSource).isRequired,
|
||||
|
||||
currentMoment: React.PropTypes.instanceOf(moment),
|
||||
|
||||
/**
|
||||
* Any extra info you want to display on the top banner of calendar
|
||||
* components
|
||||
|
@ -72,7 +74,7 @@ export default class NylasCalendar extends React.Component {
|
|||
super(props);
|
||||
this.state = {
|
||||
currentView: WEEK_VIEW,
|
||||
currentMoment: moment(),
|
||||
currentMoment: props.currentMoment || this._now(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -80,6 +82,10 @@ export default class NylasCalendar extends React.Component {
|
|||
height: "100%",
|
||||
}
|
||||
|
||||
_now() {
|
||||
return moment()
|
||||
}
|
||||
|
||||
_getCurrentViewComponent() {
|
||||
const components = {}
|
||||
components[WEEK_VIEW] = WeekView
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import _ from 'underscore'
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import moment from 'moment'
|
||||
import moment from 'moment-timezone'
|
||||
import classnames from 'classnames'
|
||||
import {Utils} from 'nylas-exports'
|
||||
|
||||
|
@ -60,6 +60,7 @@ export default class WeekView extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._mounted = true;
|
||||
this._centerScrollRegion()
|
||||
this._setIntervalHeight()
|
||||
const weekStart = moment(this.state.startMoment).add(BUFFER_DAYS, 'days').unix()
|
||||
|
@ -78,13 +79,19 @@ export default class WeekView extends React.Component {
|
|||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._mounted = false;
|
||||
this._sub.dispose();
|
||||
window.removeEventListener('resize', this._setIntervalHeight)
|
||||
}
|
||||
|
||||
// Indirection for testing purposes
|
||||
_now() {
|
||||
return moment()
|
||||
}
|
||||
|
||||
_initializeComponent(props) {
|
||||
this.todayYear = moment().year()
|
||||
this.todayDayOfYear = moment().dayOfYear()
|
||||
this.todayYear = this._now().year()
|
||||
this.todayDayOfYear = this._now().dayOfYear()
|
||||
if (this._sub) { this._sub.dispose() }
|
||||
const startMoment = this._calculateStartMoment(props)
|
||||
const endMoment = this._calculateEndMoment(props)
|
||||
|
@ -96,9 +103,21 @@ export default class WeekView extends React.Component {
|
|||
}
|
||||
|
||||
_calculateStartMoment(props) {
|
||||
const start = moment([props.currentMoment.year()])
|
||||
.weekday(0)
|
||||
.week(props.currentMoment.week())
|
||||
let start;
|
||||
|
||||
// NOTE: Since we initialize a new time from one of the properties of
|
||||
// the props.currentMomet, we need to check for the timezone!
|
||||
//
|
||||
// Other relative operations (like adding or subtracting time) are
|
||||
// independent of a timezone.
|
||||
const tz = props.currentMoment.tz()
|
||||
if (tz) {
|
||||
start = moment.tz([props.currentMoment.year()], tz)
|
||||
} else {
|
||||
start = moment([props.currentMoment.year()])
|
||||
}
|
||||
|
||||
start = start.weekday(0).week(props.currentMoment.week())
|
||||
.subtract(BUFFER_DAYS, 'days')
|
||||
return start
|
||||
}
|
||||
|
@ -233,7 +252,10 @@ export default class WeekView extends React.Component {
|
|||
|
||||
_headerComponents() {
|
||||
const left = (
|
||||
<button key="today" className="btn" onClick={this._onClickToday} style={{position: 'absolute', left: 10}}>
|
||||
<button key="today" className="btn" ref="todayBtn"
|
||||
onClick={this._onClickToday}
|
||||
style={{position: 'absolute', left: 10}}
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
);
|
||||
|
@ -242,7 +264,7 @@ export default class WeekView extends React.Component {
|
|||
}
|
||||
|
||||
_onClickToday = () => {
|
||||
this._onChangeCurrentMoment(moment())
|
||||
this._onChangeCurrentMoment(this._now())
|
||||
}
|
||||
|
||||
_onClickNextWeek = () => {
|
||||
|
@ -303,6 +325,7 @@ export default class WeekView extends React.Component {
|
|||
}
|
||||
|
||||
_setIntervalHeight = () => {
|
||||
if (!this._mounted) { return } // Resize unmounting is delayed in tests
|
||||
const wrap = ReactDOM.findDOMNode(this.refs.eventGridWrap);
|
||||
const wrapHeight = wrap.getBoundingClientRect().height;
|
||||
if (this._lastWrapHeight === wrapHeight) {
|
||||
|
@ -399,10 +422,12 @@ export default class WeekView extends React.Component {
|
|||
const days = this._daysInView();
|
||||
const eventsByDay = this._eventsByDay(days)
|
||||
const allDayOverlap = this._eventOverlap(eventsByDay.allDay);
|
||||
const tickGen = this._tickGenerator.bind(this)
|
||||
const tickGen = this._tickGenerator.bind(this);
|
||||
|
||||
return (
|
||||
<div className="calendar-view week-view">
|
||||
<CalendarEventContainer
|
||||
ref="calendarEventContainer"
|
||||
onCalendarMouseUp={this.props.onCalendarMouseUp}
|
||||
onCalendarMouseDown={this.props.onCalendarMouseDown}
|
||||
onCalendarMouseMove={this.props.onCalendarMouseMove}
|
||||
|
@ -410,6 +435,7 @@ export default class WeekView extends React.Component {
|
|||
<TopBanner bannerComponents={this.props.bannerComponents} />
|
||||
|
||||
<HeaderControls title={this._currentWeekText()}
|
||||
ref="headerControls"
|
||||
headerComponents={this._headerComponents()}
|
||||
nextAction={this._onClickNextWeek}
|
||||
prevAction={this._onClickPrevWeek}
|
||||
|
@ -437,6 +463,7 @@ export default class WeekView extends React.Component {
|
|||
</div>
|
||||
|
||||
<WeekViewAllDayEvents
|
||||
ref="weekViewAllDayEvents"
|
||||
minorDim={MIN_INTERVAL_HEIGHT}
|
||||
end={this.state.endMoment.unix()}
|
||||
height={this._allDayEventHeight(allDayOverlap)}
|
||||
|
|
Loading…
Add table
Reference in a new issue