<commit_before>var backendUtility = require('../services/backend-utility');
var memoryUtility = require('../services/memory-utility');

var location;
var dates = [];
var rating;
var purpose;

var getHotels = function(session, args, next, builder) {

    location = builder.EntityRecognizer.findEntity(args.entities, 'location');
    dates = builder.EntityRecognizer.findAllEntities(args.entities, 'builtin.datetime.date');
    rating = builder.EntityRecognizer.findEntity(args.entities, 'rating');
    purpose = builder.EntityRecognizer.findEntity(args.entities, 'purpose');
    if (!location) {
        if (memoryUtility.isFreshConversation(session.userData.stMem)) {
            var remberedHotels = memoryUtility.rememberInfoBySubject(session.userData.ltMem, 'hotel.name');
            console.log(remberedHotels);

            if (remberedHotels.length != 0) {
                session.send("Please enter the city ?")
                var msg = new builder.Message(session)
                    .textFormat(builder.TextFormat.xml)
                    .attachmentLayout(builder.AttachmentLayout.carousel)
                    .attachments([
                        new builder.HeroCard(session)
                        .buttons([
                            builder.CardAction.imBack(session, remberedHotels[0], remberedHotels[0])
                            // builder.CardAction.imBack(session, remberedHotels[1], remberedHotels[1]),
                            // builder.CardAction.imBack(session, remberedHotels[2], remberedHotels[2])
                        ])
                    ]);
            } else {
                builder.Prompts.text(session, "Please enter the city ?");
            }
        } else {
            next({ response: session.userData.stMem.content });
        }
    } else {
        next({ response: location });
    }
};
var getHotels01 = function(session, results, builder) {
    location = results.response.entity || results.response;
    memoryUtility.updateLTMem(session.userData.ltMem, { subject: 'hotel.name', location });
    if (!purpose) {
        var buttonsList = ['Business', 'Personal'];
        var msg = new builder.Message(session)
            .textFormat(builder.TextFormat.xml)
            .attachmentLayout(builder.AttachmentLayout.carousel)
            .attachments([
                new builder.HeroCard(session)
                .buttons(buttonsList)
            ]);
        builder.Prompts.choice(session, "What is your purpose ?", buttonsList);
    } else {
        next({ response: purpose });
    }
};
var getHotels02 = function(session, results, builder) {
    purpose = results.response.entity || results.response;
    if (!rating) {
        var buttonsList = ['5 star', '4 star', '3 star', 'all'];
        var msg = new builder.Message(session)
            .textFormat(builder.TextFormat.xml)
            .attachmentLayout(builder.AttachmentLayout.carousel)
            .attachments([
                new builder.HeroCard(session)
                .buttons(buttonsList)
            ]);
        builder.Prompts.choice(session, "What is your preference ?", buttonsList);
    } else {
        next({ response: rating });
    }
};
var getHotels03 = function(session, results, builder) {
    rating = results.response.entity || results.response;
    if (dates) {
        if (dates.length == 0) {
            session.dialogData.inputStatus = 0;
            builder.Prompts.text(session, "Please enter your check in date.");
        } else {
            if (dates.length == 1) {
                session.dialogData.inputStatus = 1;
                builder.Prompts.text(session, "Please enter another date.");
            } else {
                if (dates.length == 2) {
                    var hotels = backendUtility.getHotels('1234', location, rating, dates, purpose);
                    showHotels(session, hotels, builder);
                    // session.dialogData.inputStatus = 2;
                    // next({ response: rating.entity });
                }
            }
        }
    }
};
var getHotels04 = function(session, results, builder) {
    switch (session.dialogData.inputStatus) {
        case 0:
            dates.push(results.response);
            builder.Prompts.text(session, "Please enter your check out date.");
            break;
        case 1:
            dates.push(results.response);
            var hotels = backendUtility.getHotels('1234', location, rating, dates, purpose);
            showHotels(session, hotels, builder);
            break;
        default:
            session.send("Something wrong with me, I need to talk to my developer !!");

    }
};
var getHotels05 = function(session, results, builder) {
    dates.push(results.response);
    var hotels = backendUtility.getHotels('1234', location, rating, dates, purpose);
    showHotels(session, hotels, builder);
}

var showHotels = function(session, hotels, builder) {
    console.log("Inside show hotels.")

};

var bookRoom = function(session, args, next, builder) {

};
var _bookRoom = function(session, results, builder) {

};

var getBooking = function(session, args, next, builder) {

};

module.exports = {
    getHotels: getHotels,
    getHotels01: getHotels01,
    getHotels02: getHotels02,
    getHotels03: getHotels03,
    getHotels04: getHotels04,
    getHotels05: getHotels05
        // bookRoom: bookRoom,
        // _bookRoom: _bookRoom,
        // getBooking: getBooking
};<commit_msg>Automated Push<commit_after>var backendUtility = require('../services/backend-utility');
var memoryUtility = require('../services/memory-utility');

var location;
var dates = [];
var rating;
var purpose;

var getHotels = function(session, args, next, builder) {

    location = builder.EntityRecognizer.findEntity(args.entities, 'location');
    dates = builder.EntityRecognizer.findAllEntities(args.entities, 'builtin.datetime.date');
    rating = builder.EntityRecognizer.findEntity(args.entities, 'rating');
    purpose = builder.EntityRecognizer.findEntity(args.entities, 'purpose');
    if (!location) {
        if (memoryUtility.isFreshConversation(session.userData.stMem)) {
            var remberedHotels = memoryUtility.rememberInfoBySubject(session.userData.ltMem, 'hotel.name');
            console.log(remberedHotels);

            if (remberedHotels.length != 0) {
                session.send("Please enter the city ?")
                var msg = new builder.Message(session)
                    .textFormat(builder.TextFormat.xml)
                    .attachmentLayout(builder.AttachmentLayout.carousel)
                    .attachments([
                        new builder.HeroCard(session)
                        .buttons([
                            builder.CardAction.imBack(session, remberedHotels[0], remberedHotels[0])
                            // builder.CardAction.imBack(session, remberedHotels[1], remberedHotels[1]),
                            // builder.CardAction.imBack(session, remberedHotels[2], remberedHotels[2])
                        ])
                    ]);
            } else {
                builder.Prompts.text(session, "Please enter the city ?");
            }
        } else {
            next({ response: session.userData.stMem.content });
        }
    } else {
        next({ response: location });
    }
};
var getHotels01 = function(session, results, builder) {
    location = results.response.entity || results.response;
    memoryUtility.updateLTMem(session.userData.ltMem, { subject: 'hotel.name', content: location });
    if (!purpose) {
        var buttonsList = ['Business', 'Personal'];
        var msg = new builder.Message(session)
            .textFormat(builder.TextFormat.xml)
            .attachmentLayout(builder.AttachmentLayout.carousel)
            .attachments([
                new builder.HeroCard(session)
                .buttons(buttonsList)
            ]);
        builder.Prompts.choice(session, "What is your purpose ?", buttonsList);
    } else {
        next({ response: purpose });
    }
};
var getHotels02 = function(session, results, builder) {
    purpose = results.response.entity || results.response;
    if (!rating) {
        var buttonsList = ['5 star', '4 star', '3 star', 'all'];
        var msg = new builder.Message(session)
            .textFormat(builder.TextFormat.xml)
            .attachmentLayout(builder.AttachmentLayout.carousel)
            .attachments([
                new builder.HeroCard(session)
                .buttons(buttonsList)
            ]);
        builder.Prompts.choice(session, "What is your preference ?", buttonsList);
    } else {
        next({ response: rating });
    }
};
var getHotels03 = function(session, results, builder) {
    rating = results.response.entity || results.response;
    if (dates) {
        if (dates.length == 0) {
            session.dialogData.inputStatus = 0;
            builder.Prompts.text(session, "Please enter your check in date.");
        } else {
            if (dates.length == 1) {
                session.dialogData.inputStatus = 1;
                builder.Prompts.text(session, "Please enter another date.");
            } else {
                if (dates.length == 2) {
                    var hotels = backendUtility.getHotels('1234', location, rating, dates, purpose);
                    showHotels(session, hotels, builder);
                    // session.dialogData.inputStatus = 2;
                    // next({ response: rating.entity });
                }
            }
        }
    }
};
var getHotels04 = function(session, results, builder) {
    switch (session.dialogData.inputStatus) {
        case 0:
            dates.push(results.response);
            builder.Prompts.text(session, "Please enter your check out date.");
            break;
        case 1:
            dates.push(results.response);
            var hotels = backendUtility.getHotels('1234', location, rating, dates, purpose);
            showHotels(session, hotels, builder);
            break;
        default:
            session.send("Something wrong with me, I need to talk to my developer !!");

    }
};
var getHotels05 = function(session, results, builder) {
    dates.push(results.response);
    var hotels = backendUtility.getHotels('1234', location, rating, dates, purpose);
    showHotels(session, hotels, builder);
}

var showHotels = function(session, hotels, builder) {
    console.log("Inside show hotels.")

};

var bookRoom = function(session, args, next, builder) {

};
var _bookRoom = function(session, results, builder) {

};

var getBooking = function(session, args, next, builder) {

};

module.exports = {
    getHotels: getHotels,
    getHotels01: getHotels01,
    getHotels02: getHotels02,
    getHotels03: getHotels03,
    getHotels04: getHotels04,
    getHotels05: getHotels05
        // bookRoom: bookRoom,
        // _bookRoom: _bookRoom,
        // getBooking: getBooking
};<|endoftext|>


<commit_before>var chai = require('chai') // TODO Remove this when gaston allows it
var expect = chai.expect	// TODO Remove this when gaston allows it
var path = require('path')

var fs = require('../../../lib/server')

describe('fs.writeFile', function () {
	it("should handle 260-character-long file paths", function (done) {
		var fileNameLen = Math.max(260 - __dirname.length - 1, 1)
		var fileName = path.join(__dirname, new Array(fileNameLen + 1).join('x'))
		try {
		  fs.unlinkSync(fullPath)
		}
		catch (e) {
		  // Ignore.
		}
		fs.writeFile(fileName, 'ok', function (err) {
		  expect(err).not.to.exist
		  fs.unlinkSync(fileName)
		  done()
		});
	})
})<commit_msg>More tests for writeFile (from nodejs)<commit_after>var chai = require('chai') // TODO Remove this when gaston allows it
var expect = chai.expect	// TODO Remove this when gaston allows it
var path = require('path')

var fs = require('../../../lib/server')

var strContent = '南越国是前203年至前111年存在于岭南地区的一个国家，国都位于番禺，疆域包括今天中国的广东、'
  + '广西两省区的大部份地区，福建省、湖南、贵州、云南的一小部份地区和越南的北部。'
  + '南越国是秦朝灭亡后，由南海郡尉赵佗于前203年起兵兼并桂林郡和象郡后建立。'
  + '前196年和前179年，南越国曾先后两次名义上臣属于西汉，成为西汉的“外臣”。前112年，'
  + '南越国末代君主赵建德与西汉发生战争，被汉武帝于前111年所灭。南越国共存在93年，'
  + '历经五代君主。南越国是岭南地区的第一个有记载的政权国家，采用封建制和郡县制并存的制度，'
  + '它的建立保证了秦末乱世岭南地区社会秩序的稳定，有效的改善了岭南地区落后的政治、##济现状。'
  + '\n'
var bufContent = new Buffer(strContent, 'utf8')
var nbContent = 42
var tmpFilename = path.join(__dirname, 'tmp.txt')

describe('fs.writeFile', function () {
	it("should accept strings", function (done) {
		fs.writeFile(tmpFilename, strContent, function (err) {
			expect(err).not.to.exist
			fs.readFile(tmpFilename, function (err, buffer) {
				expect(err).not.to.exist
				expect(Buffer.byteLength(strContent)).to.equal(buffer.length)
				fs.unlinkSync(tmpFilename)
				done()
			})
		})
	})

	it("should accept buffers", function (done) {
		fs.writeFile(tmpFilename, bufContent, function (err) {
			expect(err).not.to.exist
			fs.readFile(tmpFilename, function (err, buf) {
				expect(err).not.to.exist
				expect(bufContent.length).to.equal(buf.length)
				fs.unlinkSync(tmpFilename)
				done()
			})
		})
	})

	it("should accept numbers", function (done) {
		fs.writeFile(tmpFilename, nbContent, { mode: 0600 }, function (err) {
			expect(err).not.to.exist
			fs.readFile(tmpFilename, function (err, buffer) {
				expect(err).not.to.exist
				expect(Buffer.byteLength('' + nbContent)).to.equal(buffer.length)
				fs.unlinkSync(tmpFilename)
				done()
			})
		})
	})

	it("should handle 260-character-long file paths", function (done) {
		var filenameLen = Math.max(260 - __dirname.length - 1, 1)
		var filename = path.join(__dirname, new Array(filenameLen + 1).join('x'))
		try {
		  fs.unlinkSync(fullPath)
		}
		catch (e) {
		  // Ignore.
		}
		fs.writeFile(filename, 'ok', function (err) {
		  expect(err).not.to.exist
		  fs.unlinkSync(filename)
		  done()
		});
	})
})<|endoftext|>


<commit_before>/* globals girderTest, describe, expect, it, runs, waitsFor, girder, _ */

girderTest.addCoveredScripts([
    '/clients/web/static/built/plugins/jobs/plugin.min.js'
]);

girderTest.importStylesheet(
    '/static/built/plugins/jobs/plugin.min.css'
);

girder.events.trigger('g:appload.before');
var app = new girder.views.App({
    el: 'body',
    parentView: null
});
girder.events.trigger('g:appload.after');

$(function () {
    describe('Unit test the job detail widget.', function () {
        it('Show a job detail widget.', function () {
            waitsFor('app to render', function () {
                return $('#g-app-body-container').length > 0;
            });

            var jobInfo = {
                _id: 'foo',
                title: 'My batch job',
                status: girder.plugins.jobs.JobStatus.INACTIVE,
                log: ['Hello world\n', 'goodbye world'],
                updated: '2015-01-12T12:00:12Z',
                created: '2015-01-12T12:00:00Z',
                when: '2015-01-12T12:00:00Z',
                timestamps: [{
                    status: girder.plugins.jobs.JobStatus.QUEUED,
                    time: '2015-01-12T12:00:02Z'
                }, {
                    status: girder.plugins.jobs.JobStatus.RUNNING,
                    time: '2015-01-12T12:00:03Z'
                }, {
                    status: girder.plugins.jobs.JobStatus.SUCCESS,
                    time: '2015-01-12T12:00:12Z'
                }]
            };

            runs(function () {
                // mock fetch to simulate fetching a job
                spyOn(girder.plugins.jobs.models.JobModel.prototype, 'fetch').andCallFake(function () {
                    this.set(jobInfo);
                    this.trigger('g:fetched');
                    return $.Deferred().resolve(jobInfo).promise();
                });

                girder.router.navigate('job/foo', {trigger: true});
            });

            waitsFor(function () {
                return $('.g-job-info-key').length > 0;
            }, 'the JobDetailsWidget to finish rendering');

            runs(function () {
                expect($('.g-monospace-viewer[property="kwargs"]').length).toBe(0);
                expect($('.g-monospace-viewer[property="log"]').text()).toBe(jobInfo.log.join(''));
                expect($('.g-job-info-value[property="_id"]').text()).toBe(jobInfo._id);
                expect($('.g-job-info-value[property="title"]').text()).toBe(jobInfo.title);
                expect($('.g-job-info-value[property="when"]').text()).toContain('January 12, 2015');
                expect($('.g-job-status-badge').text()).toContain('Inactive');

                expect($('.g-timeline-segment').length).toBe(3);
                expect($('.g-timeline-point').length).toBe(4);
                expect($('.g-timeline-start-label').text()).toBe('0 s');
                expect($('.g-timeline-end-label').text()).toBe('12 s');
                expect($('.g-timeline-point')[3].className).toContain('g-job-color-success');

                // Make sure view change happens when notification is sent for this job
                girder.utilities.eventStream.trigger('g:event.job_status', {
                    data: {
                        _id: 'foo',
                        status: girder.plugins.jobs.JobStatus.SUCCESS
                    }
                });

                expect($('.g-job-status-badge').text()).toContain('Success');

                // Make sure view change only happens for the currently viewed job
                girder.utilities.eventStream.trigger('g:event.job_status', {
                    data: {
                        _id: 'bar',
                        status: girder.plugins.jobs.JobStatus.QUEUED
                    }
                });

                expect($('.g-job-status-badge').text()).toContain('Success');

                // Test log output events
                girder.utilities.eventStream.trigger('g:event.job_log', {
                    data: {
                        _id: 'foo',
                        overwrite: true,
                        text: 'overwritten log'
                    }
                });
                expect($('.g-monospace-viewer[property="log"]').text()).toBe('overwritten log');

                girder.utilities.eventStream.trigger('g:event.job_log', {
                    data: {
                        _id: 'foo',
                        overwrite: false,
                        text: '<script type="text/javascript">xss probe!</script>'
                    }
                });

                expect($('.g-monospace-viewer[property="log"]').text()).toBe(
                    'overwritten log<script type="text/javascript">xss probe!</script>');
            });

            runs(function () {
                girder.plugins.jobs.models.JobModel.prototype.fetch.andCallThrough();
                // Return to the main page, since 'job/foo' isn't legal without mocking
                girder.router.navigate('', {trigger: true});
            });
            girderTest.waitForLoad();
        });
    });

    describe('Unit test the job list widget.', function () {
        // This spy must be attached to the prototype, since the instantiation of JobListWidget will
        // bind and make calls to '_renderData' immediately
        var renderDataSpy;
        beforeEach(function () {
            renderDataSpy = spyOn(girder.plugins.jobs.views.JobListWidget.prototype, '_renderData').andCallThrough();
        });

        it('Show a job list widget.', function () {
            var jobs, rows, widget;

            girderTest.createUser(
                'admin', 'admin@email.com', 'Quota', 'Admin', 'testpassword')();

            runs(function () {
                widget = new girder.plugins.jobs.views.JobListWidget({
                    el: $('#g-app-body-container'),
                    filter: {},
                    parentView: app,
                    showGraphs: true,
                    showFilters: true,
                    showPageSizeSelector: true
                });
            });
            waitsFor(function () {
                // Wait for the pending fetch (causing the 2nd render) to complete, so it doesn't
                // overwrite "widget.collection" after the synchronous "widget.collection.add"
                // is made below
                return renderDataSpy.callCount >= 2;
            }, 'job list to finish initial loading');

            runs(function () {
                expect($('.g-jobs-list-table>tbody>tr').length).toBe(0);

                jobs = _.map([1, 2, 3], function (i) {
                    return new girder.plugins.jobs.models.JobModel({
                        _id: 'foo' + i,
                        title: 'My batch job ' + i,
                        status: i,
                        updated: '2015-01-12T12:00:0' + i,
                        created: '2015-01-12T12:00:0' + i,
                        when: '2015-01-12T12:00:0' + i
                    });
                });

                widget.collection.add(jobs);

                // job list should re-render when collection is updated
                expect($('.g-jobs-list-table>tbody>tr').length).toBe(3)
            });

            runs(function () {
                // Make sure we are in reverse chronological order
                rows = $('.g-jobs-list-table>tbody>tr');
                expect($(rows[0]).text()).toContain('My batch job 3');
                expect($(rows[0]).text()).toContain('Success');
                expect($(rows[1]).text()).toContain('My batch job 2');
                expect($(rows[1]).text()).toContain('Running');
                expect($(rows[2]).text()).toContain('My batch job 1');
                expect($(rows[2]).text()).toContain('Queued');

                // Simulate an SSE notification that changes a job status
                girder.utilities.eventStream.trigger('g:event.job_status', {
                    data: _.extend({}, jobs[0].attributes, {
                        status: 4
                    })
                });
            });

            // Table row should update automatically
            waitsFor(function () {
                return $($('.g-jobs-list-table>tbody>tr').get(2)).find('td.g-job-status-cell').text() === 'Error';
            }, 'Third row status change to Error');

            runs(function () {
                // The data in this is meaningless, but this will trigger a new API fetch
                renderDataSpy.reset();
                girder.utilities.eventStream.trigger('g:event.job_created', {
                    data: {
                        _id: 'foo' + 4,
                        title: 'My batch job ' + 4,
                        status: 4,
                        updated: '2015-01-12T12:00:0' + 4,
                        created: '2015-01-12T12:00:0' + 4,
                        when: '2015-01-12T12:00:0' + 4
                    }
                });
            });

            waitsFor(function () {
                return renderDataSpy.wasCalled;
            });

            runs(function () {
                // Since the server contains no actual jobs, once the API fetch completes, all of
                // the previously-added local jobs will be blown away, and the local view will
                // re-render to show no jobs
                expect($('.g-no-job-record').is(':visible')).toBe(true);
            });
        });

        it('Job list widget filter by status & type.', function () {
            var widget;
            runs(function () {
                widget = new girder.plugins.jobs.views.JobListWidget({
                    el: $('#g-app-body-container'),
                    filter: {},
                    parentView: app,
                    showGraphs: true,
                    showFilters: true,
                    showPageSizeSelector: true
                });
            });
            waitsFor(function () {
                return renderDataSpy.callCount >= 2;
            }, 'job list to finish initial loading');

            runs(function () {
                expect($('.g-jobs-list-table>tbody>tr').length).toBe(0);

                // programmatically set value
                widget.typeFilterWidget.setItems({
                    'type A': true,
                    'type B': true,
                    'type C': false
                });

                // one item should be unchecked
                expect(
                    widget.$('.g-job-filter-container .type .dropdown ul li input[type="checkbox"]').toArray().reduce(function (total, input) {
                        return total + ($(input).is(':checked') ? 1 : 0);
                    }, 0)
                ).toBe(2);

                widget.$('.g-job-filter-container .type .dropdown ul li input').first().click();

                expect(
                    widget.$('.g-job-filter-container .type .dropdown ul li input[type="checkbox"]').toArray().reduce(function (total, input) {
                        return total + ($(input).is(':checked') ? 1 : 0);
                    }, 0)
                ).toBe(1);

                widget.$('.g-job-filter-container .type .dropdown .g-job-checkall input').click();

                // all should be checked after clicking Check all
                expect(
                    widget.$('.g-job-filter-container .type .dropdown ul li input[type="checkbox"]').toArray().reduce(function (total, input) {
                        return total + ($(input).is(':checked') ? 1 : 0);
                    }, 0)
                ).toBe(3);
                expect($('.g-job-filter-container .type .dropdown .g-job-checkall input').is(':checked')).toBe(true);

                widget.$('.g-job-filter-container .status .dropdown .g-job-checkall input').click();

                expect(
                    widget.$('.g-job-filter-container .status .dropdown ul li input[type="checkbox"]').toArray().reduce(function (total, input) {
                        return total + ($(input).is(':checked') ? 1 : 0);
                    }, 0)
                ).toBe(0);

                widget.$('.g-page-size').val(50).trigger('change');
                expect(widget.collection.pageLimit).toBe(50);
            });
        });

        it('Trigger click event.', function () {
            var jobs, widget;

            runs(function () {
                widget = new girder.plugins.jobs.views.JobListWidget({
                    el: $('#g-app-body-container'),
                    parentView: app,
                    filter: {},
                    triggerJobClick: true,
                    showGraphs: true,
                    showFilters: true,
                    showPageSizeSelector: true
                });
            });
            waitsFor(function () {
                return renderDataSpy.callCount >= 2;
            }, 'job list to finish initial loading');

            runs(function () {
                expect($('.g-jobs-list-table>tbody>tr').length).toBe(0);

                jobs = _.map([1, 2, 3], function (i) {
                    return new girder.plugins.jobs.models.JobModel({
                        _id: 'foo' + i,
                        title: 'My batch job ' + i,
                        status: i,
                        updated: '2015-01-12T12:00:0' + i,
                        created: '2015-01-12T12:00:0' + i,
                        when: '2015-01-12T12:00:0' + i
                    });
                });

                widget.collection.add(jobs);
            });

            waitsFor(function () {
                return $('.g-jobs-list-table>tbody>tr').length === 3;
            }, 'job list to auto-reload when collection is updated');

            runs(function () {
                var fired = false;
                widget.on('g:jobClicked', function () {
                    fired = true;
                });
                widget.$('.g-job-trigger-link').click();
                expect(fired).toBe(true);
            });
        });

        it('timing history and time chart', function () {
            var jobs, widget;
            runs(function () {
                widget = new girder.plugins.jobs.views.JobListWidget({
                    el: $('#g-app-body-container'),
                    filter: {},
                    parentView: app,
                    showGraphs: true,
                    showFilters: true,
                    showPageSizeSelector: true
                });
            });
            waitsFor(function () {
                return renderDataSpy.callCount >= 2;
            }, 'job list to finish initial loading');

            runs(function () {
                jobs = _.map(['one', 'two', 'three'], function (t, i) {
                    return new girder.plugins.jobs.models.JobModel({
                        _id: 'foo' + i,
                        title: 'My batch job ' + i,
                        status: 4,
                        type: t,
                        timestamps: [
                            {
                                'status': 1,
                                'time': '2017-03-10T18:31:59.008Z'
                            },
                            {
                                'status': 2,
                                'time': '2017-03-10T18:32:06.190Z'
                            },
                            {
                                'status': 4,
                                'time': '2017-03-10T18:32:34.760Z'
                            }
                        ],
                        updated: '2017-03-10T18:32:34.760Z',
                        created: '2017-03-10T18:31:59.008Z',
                        when: '2017-03-10T18:31:59.008Z'
                    });
                });

                widget.collection.add(jobs);

                $('.g-jobs.nav.nav-tabs li a[name="timing-history"]').tab('show');
            });

            waitsFor(function () {
                return widget.$('.g-jobs-graph svg .mark-rect.timing rect').length;
            }, 'timing history graph to render');

            runs(function () {
                $('.g-jobs.nav.nav-tabs li a[name="time"]').tab('show');
            });

            waitsFor(function () {
                return widget.$('.g-jobs-graph svg .mark-symbol.circle path').length;
            }, 'time graph to render');

            runs(function () {
                $('.g-job-filter-container .timing .dropdown .g-job-checkall input').click();
            });

            waitsFor(function () {
                return !widget.$('.g-jobs-graph svg .mark-symbol.circle path').length;
            }, 'graph to clear');
        });

        it('Instantiate without graphs, filter, and page size selector.', function () {
            var widget;

            runs(function () {
                widget = new girder.plugins.jobs.views.JobListWidget({
                    el: $('#g-app-body-container'),
                    parentView: app,
                    filter: {}
                });
            });
            waitsFor(function () {
                return renderDataSpy.callCount >= 2;
            }, 'job list to finish initial loading');

            runs(function () {
                expect(widget.$('.g-jobs.nav.nav-tabs').length).toBe(0);
                expect(widget.$('.g-job-filter-container').length).toBe(0);
                expect(widget.$('.g-page-size-container').length).toBe(0);
            });
        });
    });
});
<commit_msg>Make assertions in a much simpler way<commit_after>/* globals girderTest, describe, expect, it, runs, waitsFor, girder, _ */

girderTest.addCoveredScripts([
    '/clients/web/static/built/plugins/jobs/plugin.min.js'
]);

girderTest.importStylesheet(
    '/static/built/plugins/jobs/plugin.min.css'
);

girder.events.trigger('g:appload.before');
var app = new girder.views.App({
    el: 'body',
    parentView: null
});
girder.events.trigger('g:appload.after');

$(function () {
    describe('Unit test the job detail widget.', function () {
        it('Show a job detail widget.', function () {
            waitsFor('app to render', function () {
                return $('#g-app-body-container').length > 0;
            });

            var jobInfo = {
                _id: 'foo',
                title: 'My batch job',
                status: girder.plugins.jobs.JobStatus.INACTIVE,
                log: ['Hello world\n', 'goodbye world'],
                updated: '2015-01-12T12:00:12Z',
                created: '2015-01-12T12:00:00Z',
                when: '2015-01-12T12:00:00Z',
                timestamps: [{
                    status: girder.plugins.jobs.JobStatus.QUEUED,
                    time: '2015-01-12T12:00:02Z'
                }, {
                    status: girder.plugins.jobs.JobStatus.RUNNING,
                    time: '2015-01-12T12:00:03Z'
                }, {
                    status: girder.plugins.jobs.JobStatus.SUCCESS,
                    time: '2015-01-12T12:00:12Z'
                }]
            };

            runs(function () {
                // mock fetch to simulate fetching a job
                spyOn(girder.plugins.jobs.models.JobModel.prototype, 'fetch').andCallFake(function () {
                    this.set(jobInfo);
                    this.trigger('g:fetched');
                    return $.Deferred().resolve(jobInfo).promise();
                });

                girder.router.navigate('job/foo', {trigger: true});
            });

            waitsFor(function () {
                return $('.g-job-info-key').length > 0;
            }, 'the JobDetailsWidget to finish rendering');

            runs(function () {
                expect($('.g-monospace-viewer[property="kwargs"]').length).toBe(0);
                expect($('.g-monospace-viewer[property="log"]').text()).toBe(jobInfo.log.join(''));
                expect($('.g-job-info-value[property="_id"]').text()).toBe(jobInfo._id);
                expect($('.g-job-info-value[property="title"]').text()).toBe(jobInfo.title);
                expect($('.g-job-info-value[property="when"]').text()).toContain('January 12, 2015');
                expect($('.g-job-status-badge').text()).toContain('Inactive');

                expect($('.g-timeline-segment').length).toBe(3);
                expect($('.g-timeline-point').length).toBe(4);
                expect($('.g-timeline-start-label').text()).toBe('0 s');
                expect($('.g-timeline-end-label').text()).toBe('12 s');
                expect($('.g-timeline-point')[3].className).toContain('g-job-color-success');

                // Make sure view change happens when notification is sent for this job
                girder.utilities.eventStream.trigger('g:event.job_status', {
                    data: {
                        _id: 'foo',
                        status: girder.plugins.jobs.JobStatus.SUCCESS
                    }
                });

                expect($('.g-job-status-badge').text()).toContain('Success');

                // Make sure view change only happens for the currently viewed job
                girder.utilities.eventStream.trigger('g:event.job_status', {
                    data: {
                        _id: 'bar',
                        status: girder.plugins.jobs.JobStatus.QUEUED
                    }
                });

                expect($('.g-job-status-badge').text()).toContain('Success');

                // Test log output events
                girder.utilities.eventStream.trigger('g:event.job_log', {
                    data: {
                        _id: 'foo',
                        overwrite: true,
                        text: 'overwritten log'
                    }
                });
                expect($('.g-monospace-viewer[property="log"]').text()).toBe('overwritten log');

                girder.utilities.eventStream.trigger('g:event.job_log', {
                    data: {
                        _id: 'foo',
                        overwrite: false,
                        text: '<script type="text/javascript">xss probe!</script>'
                    }
                });

                expect($('.g-monospace-viewer[property="log"]').text()).toBe(
                    'overwritten log<script type="text/javascript">xss probe!</script>');
            });

            runs(function () {
                girder.plugins.jobs.models.JobModel.prototype.fetch.andCallThrough();
                // Return to the main page, since 'job/foo' isn't legal without mocking
                girder.router.navigate('', {trigger: true});
            });
            girderTest.waitForLoad();
        });
    });

    describe('Unit test the job list widget.', function () {
        // This spy must be attached to the prototype, since the instantiation of JobListWidget will
        // bind and make calls to '_renderData' immediately
        var renderDataSpy;
        beforeEach(function () {
            renderDataSpy = spyOn(girder.plugins.jobs.views.JobListWidget.prototype, '_renderData').andCallThrough();
        });

        it('Show a job list widget.', function () {
            var jobs, rows, widget;

            girderTest.createUser(
                'admin', 'admin@email.com', 'Quota', 'Admin', 'testpassword')();

            runs(function () {
                widget = new girder.plugins.jobs.views.JobListWidget({
                    el: $('#g-app-body-container'),
                    filter: {},
                    parentView: app,
                    showGraphs: true,
                    showFilters: true,
                    showPageSizeSelector: true
                });
            });
            waitsFor(function () {
                // Wait for the pending fetch (causing the 2nd render) to complete, so it doesn't
                // overwrite "widget.collection" after the synchronous "widget.collection.add"
                // is made below
                return renderDataSpy.callCount >= 2;
            }, 'job list to finish initial loading');

            runs(function () {
                expect($('.g-jobs-list-table>tbody>tr').length).toBe(0);

                jobs = _.map([1, 2, 3], function (i) {
                    return new girder.plugins.jobs.models.JobModel({
                        _id: 'foo' + i,
                        title: 'My batch job ' + i,
                        status: i,
                        updated: '2015-01-12T12:00:0' + i,
                        created: '2015-01-12T12:00:0' + i,
                        when: '2015-01-12T12:00:0' + i
                    });
                });

                widget.collection.add(jobs);

                // job list should re-render when collection is updated
                expect($('.g-jobs-list-table>tbody>tr').length).toBe(3)
            });

            runs(function () {
                // Make sure we are in reverse chronological order
                rows = $('.g-jobs-list-table>tbody>tr');
                expect($(rows[0]).text()).toContain('My batch job 3');
                expect($(rows[0]).text()).toContain('Success');
                expect($(rows[1]).text()).toContain('My batch job 2');
                expect($(rows[1]).text()).toContain('Running');
                expect($(rows[2]).text()).toContain('My batch job 1');
                expect($(rows[2]).text()).toContain('Queued');

                // Simulate an SSE notification that changes a job status
                girder.utilities.eventStream.trigger('g:event.job_status', {
                    data: _.extend({}, jobs[0].attributes, {
                        status: 4
                    })
                });
            });

            // Table row should update automatically
            waitsFor(function () {
                return $($('.g-jobs-list-table>tbody>tr').get(2)).find('td.g-job-status-cell').text() === 'Error';
            }, 'Third row status change to Error');

            runs(function () {
                // The data in this is meaningless, but this will trigger a new API fetch
                renderDataSpy.reset();
                girder.utilities.eventStream.trigger('g:event.job_created', {
                    data: {
                        _id: 'foo' + 4,
                        title: 'My batch job ' + 4,
                        status: 4,
                        updated: '2015-01-12T12:00:0' + 4,
                        created: '2015-01-12T12:00:0' + 4,
                        when: '2015-01-12T12:00:0' + 4
                    }
                });
            });

            waitsFor(function () {
                return renderDataSpy.wasCalled;
            });

            runs(function () {
                // Since the server contains no actual jobs, once the API fetch completes, all of
                // the previously-added local jobs will be blown away, and the local view will
                // re-render to show no jobs
                expect($('.g-no-job-record').is(':visible')).toBe(true);
            });
        });

        it('Job list widget filter by status & type.', function () {
            var widget;
            runs(function () {
                widget = new girder.plugins.jobs.views.JobListWidget({
                    el: $('#g-app-body-container'),
                    filter: {},
                    parentView: app,
                    showGraphs: true,
                    showFilters: true,
                    showPageSizeSelector: true
                });
            });
            waitsFor(function () {
                return renderDataSpy.callCount >= 2;
            }, 'job list to finish initial loading');

            runs(function () {
                expect($('.g-jobs-list-table>tbody>tr').length).toBe(0);

                // programmatically set value
                widget.typeFilterWidget.setItems({
                    'type A': true,
                    'type B': true,
                    'type C': false
                });

                // one item should be unchecked
                expect(widget.$('.g-job-filter-container .type .dropdown ul li input[type="checkbox"]:checked').length).toBe(2);

                widget.$('.g-job-filter-container .type .dropdown ul li input').first().click();

                expect(widget.$('.g-job-filter-container .type .dropdown ul li input[type="checkbox"]:checked').length).toBe(1);

                widget.$('.g-job-filter-container .type .dropdown .g-job-checkall input').click();

                // all should be checked after clicking Check all
                expect(widget.$('.g-job-filter-container .type .dropdown ul li input[type="checkbox"]:checked').length).toBe(3);
                expect($('.g-job-filter-container .type .dropdown .g-job-checkall input').is(':checked')).toBe(true);

                widget.$('.g-job-filter-container .status .dropdown .g-job-checkall input').click();

                expect(widget.$('.g-job-filter-container .status .dropdown ul li input[type="checkbox"]:checked').length).toBe(0);

                widget.$('.g-page-size').val(50).trigger('change');
                expect(widget.collection.pageLimit).toBe(50);
            });
        });

        it('Trigger click event.', function () {
            var jobs, widget;

            runs(function () {
                widget = new girder.plugins.jobs.views.JobListWidget({
                    el: $('#g-app-body-container'),
                    parentView: app,
                    filter: {},
                    triggerJobClick: true,
                    showGraphs: true,
                    showFilters: true,
                    showPageSizeSelector: true
                });
            });
            waitsFor(function () {
                return renderDataSpy.callCount >= 2;
            }, 'job list to finish initial loading');

            runs(function () {
                expect($('.g-jobs-list-table>tbody>tr').length).toBe(0);

                jobs = _.map([1, 2, 3], function (i) {
                    return new girder.plugins.jobs.models.JobModel({
                        _id: 'foo' + i,
                        title: 'My batch job ' + i,
                        status: i,
                        updated: '2015-01-12T12:00:0' + i,
                        created: '2015-01-12T12:00:0' + i,
                        when: '2015-01-12T12:00:0' + i
                    });
                });

                widget.collection.add(jobs);
            });

            waitsFor(function () {
                return $('.g-jobs-list-table>tbody>tr').length === 3;
            }, 'job list to auto-reload when collection is updated');

            runs(function () {
                var fired = false;
                widget.on('g:jobClicked', function () {
                    fired = true;
                });
                widget.$('.g-job-trigger-link').click();
                expect(fired).toBe(true);
            });
        });

        it('timing history and time chart', function () {
            var jobs, widget;
            runs(function () {
                widget = new girder.plugins.jobs.views.JobListWidget({
                    el: $('#g-app-body-container'),
                    filter: {},
                    parentView: app,
                    showGraphs: true,
                    showFilters: true,
                    showPageSizeSelector: true
                });
            });
            waitsFor(function () {
                return renderDataSpy.callCount >= 2;
            }, 'job list to finish initial loading');

            runs(function () {
                jobs = _.map(['one', 'two', 'three'], function (t, i) {
                    return new girder.plugins.jobs.models.JobModel({
                        _id: 'foo' + i,
                        title: 'My batch job ' + i,
                        status: 4,
                        type: t,
                        timestamps: [
                            {
                                'status': 1,
                                'time': '2017-03-10T18:31:59.008Z'
                            },
                            {
                                'status': 2,
                                'time': '2017-03-10T18:32:06.190Z'
                            },
                            {
                                'status': 4,
                                'time': '2017-03-10T18:32:34.760Z'
                            }
                        ],
                        updated: '2017-03-10T18:32:34.760Z',
                        created: '2017-03-10T18:31:59.008Z',
                        when: '2017-03-10T18:31:59.008Z'
                    });
                });

                widget.collection.add(jobs);

                $('.g-jobs.nav.nav-tabs li a[name="timing-history"]').tab('show');
            });

            waitsFor(function () {
                return widget.$('.g-jobs-graph svg .mark-rect.timing rect').length;
            }, 'timing history graph to render');

            runs(function () {
                $('.g-jobs.nav.nav-tabs li a[name="time"]').tab('show');
            });

            waitsFor(function () {
                return widget.$('.g-jobs-graph svg .mark-symbol.circle path').length;
            }, 'time graph to render');

            runs(function () {
                $('.g-job-filter-container .timing .dropdown .g-job-checkall input').click();
            });

            waitsFor(function () {
                return !widget.$('.g-jobs-graph svg .mark-symbol.circle path').length;
            }, 'graph to clear');
        });

        it('Instantiate without graphs, filter, and page size selector.', function () {
            var widget;

            runs(function () {
                widget = new girder.plugins.jobs.views.JobListWidget({
                    el: $('#g-app-body-container'),
                    parentView: app,
                    filter: {}
                });
            });
            waitsFor(function () {
                return renderDataSpy.callCount >= 2;
            }, 'job list to finish initial loading');

            runs(function () {
                expect(widget.$('.g-jobs.nav.nav-tabs').length).toBe(0);
                expect(widget.$('.g-job-filter-container').length).toBe(0);
                expect(widget.$('.g-page-size-container').length).toBe(0);
            });
        });
    });
});
<|endoftext|>


<commit_before>import hashTracking from '../../src/plugins/hashTracking';

describe('hashTracking', () => {
  let Simpla,
      hashObserver;

  beforeEach(() => {
    Simpla = {
      observe: sinon.stub(),
      startEditing: sinon.stub(),
      stopEditing: sinon.stub()
    };

    sinon.stub(window, 'addEventListener', (ev, callback) => {
      hashObserver = callback;
    });
  });

  afterEach(() => {
    window.location.hash = '';
    window.addEventListener.restore();
  });

  it('should observe editing and change hash accordingly', () => {
    let property,
        callback;

    hashTracking(Simpla);
    [ property, callback ] = Simpla.observe.lastCall.args;

    expect(property).to.equal('editing');

    callback(true);
    expect(window.location.hash).to.equal('#edit');

    callback(false);
    expect(window.location.hash).to.equal('');
  });

  it('should listen for changes on the window and update Simpla accordingly', () => {
    let editingWindow = {
          location: { hash: '#edit' }
        },
        notEditingWindow = {
          location: { hash: '' }
        };

    hashTracking(Simpla);

    // Check it listens
    expect(window.addEventListener.calledWith('hashchange')).to.be.true;

    // Should initialize things
    expect(Simpla.stopEditing.callCount).to.equal(1);

    hashObserver({ target: editingWindow });
    expect(Simpla.startEditing.callCount).to.equal(1);

    hashObserver({ target: notEditingWindow });
    expect(Simpla.stopEditing.callCount).to.equal(2);
  });
});
<commit_msg>check for inequality with #edit, fixes cross browser hash checking<commit_after>import hashTracking from '../../src/plugins/hashTracking';

describe('hashTracking', () => {
  let Simpla,
      hashObserver;

  beforeEach(() => {
    Simpla = {
      observe: sinon.stub(),
      startEditing: sinon.stub(),
      stopEditing: sinon.stub()
    };

    sinon.stub(window, 'addEventListener', (ev, callback) => {
      hashObserver = callback;
    });
  });

  afterEach(() => {
    window.location.hash = '';
    window.addEventListener.restore();
  });

  it('should observe editing and change hash accordingly', () => {
    let property,
        callback;

    hashTracking(Simpla);
    [ property, callback ] = Simpla.observe.lastCall.args;

    expect(property).to.equal('editing');

    callback(true);
    expect(window.location.hash).to.equal('#edit');

    callback(false);
    expect(window.location.hash).to.not.equal('#edit');
  });

  it('should listen for changes on the window and update Simpla accordingly', () => {
    let editingWindow = {
          location: { hash: '#edit' }
        },
        notEditingWindow = {
          location: { hash: '' }
        };

    hashTracking(Simpla);

    // Check it listens
    expect(window.addEventListener.calledWith('hashchange')).to.be.true;

    // Should initialize things
    expect(Simpla.stopEditing.callCount).to.equal(1);

    hashObserver({ target: editingWindow });
    expect(Simpla.startEditing.callCount).to.equal(1);

    hashObserver({ target: notEditingWindow });
    expect(Simpla.stopEditing.callCount).to.equal(2);
  });
});
<|endoftext|>


<commit_before>d3.svg.tip = function() {
  var orient = 'top',
      padding = 5,
      cornerRadius = 2,
      stemSize = 50,
      offset = d3_svg_offset,
      text = d3_svg_text,
      node = make('g');

  function tip(d, i) {
    this.ownerSVGElement.appendChild(node);

    var tipOffset = offset.apply(this, arguments),
        tipText   = text.apply(this, arguments),
        container = d3.select(node),
        tag = this.tagName.toLowerCase(),
        x, y, stem, backingRect, containerRect, stemRect;

    // Elements and Bounds
    var doc        = d3.select(this.ownerSVGElement),
        target     = d3.select(this),
        backing    = d3.select(make('rect')),
        docRect    = this.ownerSVGElement.getBoundingClientRect(),
        targetRect = this.getBBox();

    // FIXME: d3 has problems using `append` with nodes that were created
    // but not immediately added to the SVG dom.
    // Clear the container and add the rect backing
    container.text(' ').node().appendChild(backing.node())

    // The value to show in the tooltip
    var val = container.append('text').text(tipText).attr('text-anchor', 'middle').attr('alignment-baseline', 'middle'),
        valRect = val.node().getBBox();

    valRect.width = valRect.width + (padding * 2)
    valRect.height = valRect.height + (padding * 2)

    backing.attr('width', valRect.width)
      .attr('height', valRect.height)
      .attr('rx', cornerRadius)
      .attr('ry', cornerRadius)

    val.attr('dx', valRect.width / 2).attr('dy', valRect.height / 2)

    backingRect = backing.node().getBBox()

    // TODO: stem seems to report the wrong height, so it's never completely flush
    // against the backing rect.
    stem = container.append('path').attr('d', d3_svg_stem())
    stemRect = stem.node().getBBox()

    switch(orient) {
      case 'top':
        stem.attr('transform', 'translate(' + (backingRect.width / 2) + ',' + backingRect.height + ')');

        containerRect = container.node().getBBox()
        x = targetRect.x + (targetRect.width / 2) - (containerRect.width / 2) + tipOffset[0];
        y = targetRect.y - containerRect.height + tipOffset[1];
      break;
      case 'bottom':
        stem.attr('transform', 'translate(' + (backingRect.width / 2) + ',' + -(stemRect.height / 2) + ')');

        containerRect = container.node().getBBox()
        x = targetRect.x + (targetRect.width / 2) - (containerRect.width / 2) + tipOffset[0];
        y = targetRect.y + targetRect.height + stemRect.height - tipOffset[1];
      break;
      case 'left':
        stem.attr('transform', 'translate(' + backingRect.width + ',' + (backingRect.height / 2) + ') rotate(-90)');

        containerRect = container.node().getBBox()
        x = targetRect.x - (stemRect.height / 2) + tipOffset[0];
        y = targetRect.y + tipOffset[1];

        if(tag == 'circle') {
          x -= targetRect.width + (containerRect.width / 2);
          y -= targetRect.height / 2
        } else if(tag == 'rect') {
          x -= containerRect.width - (stemRect.height / 2)
          y -= containerRect.height / 2
        }

      break;
      case 'right':
        stem.attr('transform', 'translate(' + -(stemRect.height / 2) + ',' + (backingRect.height / 2) + ') rotate(90)');

        containerRect = container.node().getBBox()
        x = targetRect.x + stemRect.height + tipOffset[0];
        y = targetRect.y + tipOffset[1];

        if(tag == 'circle') {
          x += targetRect.width
          y -= targetRect.height / 2
        } else if(tag == 'rect') {
          x += targetRect.width
          y -= containerRect.height / 2
        }
      break;
    }

    container.attr('transform', 'translate(' + x + ',' + y + ')')
  }

  function d3_svg_offset() {
    return [0, 0];
  }

  function d3_svg_text() {
    return ' ';
  }

  function d3_svg_stem() {
    return d3.svg.symbol().type(orient == 'bottom' ? 'triangle-up' : 'triangle-down').size(stemSize);
  }

  tip.attr = function(n, v) {
    d3.select(node).attr(n, v)
    return tip;
  }

  tip.orient = function(v) {
    if (!arguments.length) return orient;
    orient = v;
    return tip;
  };

  tip.padding = function(v) {
    if (!arguments.length) return padding;
    padding = v;
    return tip;
  };

  tip.cornerRadius = function(v) {
    if (!arguments.length) return cornerRadius;
    cornerRadius = v;
    return tip;
  };

  tip.stemSize = function(v) {
    if (!arguments.length) return stemSize;
    stemSize = v;
    return tip;
  };

  tip.offset = function(v) {
    if (!arguments.length) return offset;
    offset = v == null ? v: d3.functor(v);
    return tip;
  };

  tip.text = function(v) {
    if (!arguments.length) return text;
    text = v == null ? v: d3.functor(v);

    return tip;
  };

  function make(e) {
    return document.createElementNS(d3.ns.prefix.svg, e);
  }

  return tip;
}<commit_msg>move orientation types into reusable methods<commit_after>d3.svg.tip = function() {
  var orient = 'top',
      padding = 5,
      cornerRadius = 2,
      stemSize = 50,
      offset = d3_svg_offset,
      text = d3_svg_text,
      node = make('g');

  function tip(d, i) {
    this.ownerSVGElement.appendChild(node);

    var tipOffset = offset.apply(this, arguments),
        tipText   = text.apply(this, arguments),
        container = d3.select(node),
        tag = this.tagName.toLowerCase(),
        loc, stem, backingRect, containerRect, stemRect, d3_orient_types;

    // Elements and Bounds
    var doc        = d3.select(this.ownerSVGElement),
        target     = d3.select(this),
        backing    = d3.select(make('rect')),
        docRect    = this.ownerSVGElement.getBoundingClientRect(),
        targetRect = this.getBBox();

    // FIXME: d3 has problems using `append` with nodes that were created
    // but not immediately added to the SVG dom.
    // Clear the container and add the rect backing
    container.text(' ').node().appendChild(backing.node())

    // The value to show in the tooltip
    var val = container.append('text').text(tipText).attr('text-anchor', 'middle').attr('alignment-baseline', 'middle'),
        valRect = val.node().getBBox();

    valRect.width = valRect.width + (padding * 2)
    valRect.height = valRect.height + (padding * 2)

    backing.attr('width', valRect.width)
      .attr('height', valRect.height)
      .attr('rx', cornerRadius)
      .attr('ry', cornerRadius)

    val.attr('dx', valRect.width / 2).attr('dy', valRect.height / 2)

    backingRect = backing.node().getBBox()

    // TODO: stem seems to report the wrong height, so it's never completely flush
    // against the backing rect.
    stem = container.append('path').attr('d', d3_svg_stem())
    stemRect = stem.node().getBBox()

    d3_orient_types = {
      top: function() {
        stem.attr('transform', 'translate(' + (backingRect.width / 2) + ',' + backingRect.height + ')');
        containerRect = container.node().getBBox()
        x = targetRect.x + (targetRect.width / 2) - (containerRect.width / 2) + tipOffset[0];
        y = targetRect.y - containerRect.height + tipOffset[1];

        return {x: x, y: y}
      },

      bottom: function() {
        stem.attr('transform', 'translate(' + (backingRect.width / 2) + ',' + -(stemRect.height / 2) + ')');

        containerRect = container.node().getBBox()
        x = targetRect.x + (targetRect.width / 2) - (containerRect.width / 2) + tipOffset[0];
        y = targetRect.y + targetRect.height + stemRect.height - tipOffset[1]

        return {x: x, y: y}
      },

      left: function() {
        stem.attr('transform', 'translate(' + backingRect.width + ',' + (backingRect.height / 2) + ') rotate(-90)');

        containerRect = container.node().getBBox()
        x = targetRect.x - (stemRect.height / 2) + tipOffset[0];
        y = targetRect.y + tipOffset[1];

        if(tag == 'circle') {
          x -= targetRect.width + (containerRect.width / 2);
          y -= targetRect.height / 2
        } else if(tag == 'rect') {
          x -= containerRect.width - (stemRect.height / 2)
          y -= containerRect.height / 2
        }

        return {x: x, y: y}
      },

      right: function() {
        stem.attr('transform', 'translate(' + -(stemRect.height / 2) + ',' + (backingRect.height / 2) + ') rotate(90)');

        containerRect = container.node().getBBox()
        x = targetRect.x + stemRect.height + tipOffset[0];
        y = targetRect.y + tipOffset[1];

        if(tag == 'circle') {
          x += targetRect.width
          y -= targetRect.height / 2
        } else if(tag == 'rect') {
          x += targetRect.width
          y -= containerRect.height / 2
        }

        return {x: x, y: y}
      }
    }

    loc = d3_orient_types[orient]()
    container.attr('transform', 'translate(' + loc.x + ',' + loc.y + ')')
  }

  function d3_svg_offset() {
    return [0, 0];
  }

  function d3_svg_text() {
    return ' ';
  }

  function d3_svg_stem() {
    return d3.svg.symbol().type(orient == 'bottom' ? 'triangle-up' : 'triangle-down').size(stemSize);
  }

  tip.attr = function(n, v) {
    d3.select(node).attr(n, v)
    return tip;
  }

  tip.orient = function(v) {
    if (!arguments.length) return orient;
    orient = v;
    return tip;
  };

  tip.padding = function(v) {
    if (!arguments.length) return padding;
    padding = v;
    return tip;
  };

  tip.cornerRadius = function(v) {
    if (!arguments.length) return cornerRadius;
    cornerRadius = v;
    return tip;
  };

  tip.stemSize = function(v) {
    if (!arguments.length) return stemSize;
    stemSize = v;
    return tip;
  };

  tip.offset = function(v) {
    if (!arguments.length) return offset;
    offset = v == null ? v: d3.functor(v);
    return tip;
  };

  tip.text = function(v) {
    if (!arguments.length) return text;
    text = v == null ? v: d3.functor(v);

    return tip;
  };

  function make(e) {
    return document.createElementNS(d3.ns.prefix.svg, e);
  }

  return tip;
}<|endoftext|>


<commit_before>'use strict';

import chai from 'chai';
import RssFeedEmitter from '../src/rss-feed-emitter.js';

let expect = chai.expect;

describe('RssFeedEmitter', () => {

	describe('quando instanciado', () => {

		let feeder = new RssFeedEmitter();

		it('deve retornar um objeto', () => {
			expect( feeder ).to.be.an('object');
		});

		it('#remove deve ser uma função', () => {
			expect( feeder.remove ).to.be.a('function');
		});

		it('#on deve ser uma função', () => {
			expect( feeder.on ).to.be.a('function');
		});

		it('#emit deve ser uma função', () => {
			expect( feeder.emit ).to.be.a('function');
		});

		it('#destroy deve ser uma função', () => {
			expect( feeder.destroy ).to.be.a('function');
		});

	})

	describe('#add', () => {

		it('deve ser uma função', () => {

			let feeder = new RssFeedEmitter();
			expect( feeder.add ).to.be.a('function');

		});

		it('deve retornar erro quando chamado sem objeto de configuração', () => {

			let feeder = new RssFeedEmitter();
			expect( () => feeder.add() ).to.throw(Error);

		});

		it('deve retornar erro quando objeto de configuração não possui "url"', () => {

			let feeder = new RssFeedEmitter();
			expect( () => {
				feeder.add({
					refresh: 60000
				})
			}).to.throw(Error);

		});

		it('deve retornar erro quando objeto de configuração possui "url" mas não é uma string', () => {

			let feeder = new RssFeedEmitter();

			expect( () => {
				feeder.add({
					url: [1, 2, 3]
				})
			}).to.throw(Error);

		});

		it('deve retornar erro quando objeto de configuração possui "refresh" mas não um número', () => {

			let feeder = new RssFeedEmitter();

			expect( () => {
				feeder.add({
					url: 'http://www.nintendolife.com/feeds/latest',
					refresh: 'quickly'
				});
			}).to.throw(Error);

		});

		it('deve adicionar corretamente feeds quando possuir somente "url"', () => {

			let feeder = new RssFeedEmitter();

			feeder.add({
				url: 'http://www.nintendolife.com/feeds/latest',
			});

			feeder.add({
				url: 'http://www.nintendolife.com/feeds/news',
			});

			expect( feeder.list() ).to.have.property('length', 2);
			expect( feeder.list()[0] ).to.have.property('refresh', 60000);
			expect( feeder.list()[1] ).to.have.property('refresh', 60000);

		});

		it('deve substituir a taxa de atualização padrão quando possuir "refresh"', () => {

			let feeder = new RssFeedEmitter();

			feeder.add({
				url: 'http://www.nintendolife.com/feeds/latest',
				refresh: 120000
			});

			expect( feeder.list()[0] ).to.have.property('refresh', 120000);

		});

		it('deve atualizar o feed quando "url" já existir na lista de feeds', () => {

			let feeder = new RssFeedEmitter();

			feeder.add({
				url: 'http://www.nintendolife.com/feeds/latest',
				refresh: 120000
			});

			expect( feeder.list() ).to.have.property('length', 1);
			expect( feeder.list()[0] ).to.have.property('refresh', 120000);

			feeder.add({
				url: 'http://www.nintendolife.com/feeds/latest',
				refresh: 240000
			});

			expect( feeder.list() ).to.have.property('length', 1);
			expect( feeder.list()[0] ).to.have.property('refresh', 240000);

		});

	})

	describe('#list', () => {

		it('deve ser uma função', () => {

			let feeder = new RssFeedEmitter();
			expect( feeder.list ).to.be.a('function');

		});

		it('deve retornar um array em branco por default', () => {

			let feeder = new RssFeedEmitter();
			let list = feeder.list();

			expect( list ).to.be.an('array');
			expect( list ).to.have.property('length', 0);

		});

		it('deve listar todos os feeds cadastrados', () => {

			let feeder = new RssFeedEmitter();

			feeder.add({
				url: 'http://www.nintendolife.com/feeds/latest',
				refresh: 2000
			});

			feeder.add({
				url: 'http://www.nintendolife.com/feeds/news',
				refresh: 5000
			});

			let list = feeder.list();

			expect( list ).to.have.property('length', 2);

			expect( list[0] ).to.have.property('url', 'http://www.nintendolife.com/feeds/latest');
			expect( list[0] ).to.have.property('refresh', 2000);
			expect( list[0] ).to.have.property('setInterval');

			expect( list[1] ).to.have.property('url', 'http://www.nintendolife.com/feeds/news');
			expect( list[1] ).to.have.property('refresh', 5000);
			expect( list[1] ).to.have.property('setInterval');

		})

	})

})
<commit_msg>test(destroy): inicia os testes do método #destroy<commit_after>'use strict';

import chai from 'chai';
import RssFeedEmitter from '../src/rss-feed-emitter.js';

let expect = chai.expect;

describe('RssFeedEmitter', () => {

	describe('quando instanciado', () => {

		let feeder = new RssFeedEmitter();

		it('deve retornar um objeto', () => {
			expect( feeder ).to.be.an('object');
		});

		it('#remove deve ser uma função', () => {
			expect( feeder.remove ).to.be.a('function');
		});

		it('#on deve ser uma função', () => {
			expect( feeder.on ).to.be.a('function');
		});

		it('#emit deve ser uma função', () => {
			expect( feeder.emit ).to.be.a('function');
		});

	})

	describe('#add', () => {

		it('deve ser uma função', () => {

			let feeder = new RssFeedEmitter();
			expect( feeder.add ).to.be.a('function');

		});

		it('deve retornar erro quando chamado sem objeto de configuração', () => {

			let feeder = new RssFeedEmitter();
			expect( () => feeder.add() ).to.throw(Error);

		});

		it('deve retornar erro quando objeto de configuração não possui "url"', () => {

			let feeder = new RssFeedEmitter();
			expect( () => {
				feeder.add({
					refresh: 60000
				})
			}).to.throw(Error);

		});

		it('deve retornar erro quando objeto de configuração possui "url" mas não é uma string', () => {

			let feeder = new RssFeedEmitter();

			expect( () => {
				feeder.add({
					url: [1, 2, 3]
				})
			}).to.throw(Error);

		});

		it('deve retornar erro quando objeto de configuração possui "refresh" mas não um número', () => {

			let feeder = new RssFeedEmitter();

			expect( () => {
				feeder.add({
					url: 'http://www.nintendolife.com/feeds/latest',
					refresh: 'quickly'
				});
			}).to.throw(Error);

		});

		it('deve adicionar corretamente feeds quando possuir somente "url"', () => {

			let feeder = new RssFeedEmitter();

			feeder.add({
				url: 'http://www.nintendolife.com/feeds/latest',
			});

			feeder.add({
				url: 'http://www.nintendolife.com/feeds/news',
			});

			expect( feeder.list() ).to.have.property('length', 2);
			expect( feeder.list()[0] ).to.have.property('refresh', 60000);
			expect( feeder.list()[1] ).to.have.property('refresh', 60000);

		});

		it('deve substituir a taxa de atualização padrão quando possuir "refresh"', () => {

			let feeder = new RssFeedEmitter();

			feeder.add({
				url: 'http://www.nintendolife.com/feeds/latest',
				refresh: 120000
			});

			expect( feeder.list()[0] ).to.have.property('refresh', 120000);

		});

		it('deve atualizar o feed quando "url" já existir na lista de feeds', () => {

			let feeder = new RssFeedEmitter();

			feeder.add({
				url: 'http://www.nintendolife.com/feeds/latest',
				refresh: 120000
			});

			expect( feeder.list() ).to.have.property('length', 1);
			expect( feeder.list()[0] ).to.have.property('refresh', 120000);

			feeder.add({
				url: 'http://www.nintendolife.com/feeds/latest',
				refresh: 240000
			});

			expect( feeder.list() ).to.have.property('length', 1);
			expect( feeder.list()[0] ).to.have.property('refresh', 240000);

		});

	})

	describe('#list', () => {

		it('deve ser uma função', () => {

			let feeder = new RssFeedEmitter();
			expect( feeder.list ).to.be.a('function');

		});

		it('deve retornar um array em branco por default', () => {

			let feeder = new RssFeedEmitter();
			let list = feeder.list();

			expect( list ).to.be.an('array');
			expect( list ).to.have.property('length', 0);

		});

		it('deve listar todos os feeds cadastrados', () => {

			let feeder = new RssFeedEmitter();

			feeder.add({
				url: 'http://www.nintendolife.com/feeds/latest',
				refresh: 2000
			});

			feeder.add({
				url: 'http://www.nintendolife.com/feeds/news',
				refresh: 5000
			});

			let list = feeder.list();

			expect( list ).to.have.property('length', 2);

			expect( list[0] ).to.have.property('url', 'http://www.nintendolife.com/feeds/latest');
			expect( list[0] ).to.have.property('refresh', 2000);
			expect( list[0] ).to.have.property('setInterval');

			expect( list[1] ).to.have.property('url', 'http://www.nintendolife.com/feeds/news');
			expect( list[1] ).to.have.property('refresh', 5000);
			expect( list[1] ).to.have.property('setInterval');

		})

	})

	describe('#destroy', () => {

		it('#deve ser uma função', () => {
			let feeder = new RssFeedEmitter();
			expect( feeder.destroy ).to.be.a('function');
		});

	})

})
<|endoftext|>


<commit_before>Tinytest.add("session hooks - identifying a human", function() {

});

Tinytest.add("session - update stats", function() {

});

Tinytest.add("session - update stats and blocked", function() {

});<commit_msg>Add tests for session hooks<commit_after>var sessionProto = MeteorX.Session.prototype;
Tinytest.add("session hooks - identifying a human", function(test) {
  var context = {
    send: sinon.stub(),
    socket: {
      headers: {},
      remoteAddress: Random.id()
    }
  };
  var humanToken = Random.id();
  var msg = {
    msg: "method",
    method: "setFirewallHumanToken",
    params: [humanToken]
  };

  sessionProto.processMessage.call(context, msg);
  test.equal(context._firewallHumanToken, humanToken);
  test.equal(context.send.callCount, 2);
});

Tinytest.add("session hooks - update stats", function(test) {
  var ip = Random.id();
  var sessionId = Random.id();
  var humanToken = Random.id();

  var context = {
    send: sinon.stub(),
    socket: {
      headers: {},
      remoteAddress: ip
    },
    id: sessionId,
    _firewallHumanToken: humanToken
  };

  var msg = {
    msg: "method",
    method: "not-existing",
    params: [10]
  };

  var newFirewall = {
    _updateStats: sinon.stub()
  };

  WithNew(Firewall, newFirewall, function() {
    sessionProto.processMessage.call(context, msg);
    test.equal(newFirewall._updateStats.callCount, 1);
    test.equal(newFirewall._updateStats.args[0], [ip, sessionId, humanToken]);
  });
});

Tinytest.add("session hooks - update stats and blocked", function(test) {
  var context = {
    send: sinon.stub(),
    socket: {
      headers: {},
      removeAllListeners: sinon.stub()
    }
  };

  var msg = {
    msg: "method",
    method: "not-existing",
    params: [10]
  };

  var newFirewall = {
    _updateStats: sinon.stub()
  };

  WithNew(Firewall, newFirewall, function() {
    newFirewall._updateStats.onCall(0).returns(true);
    sessionProto.processMessage.call(context, msg);
    test.equal(context.send.callCount, 1);
    test.equal(context.socket.removeAllListeners.callCount, 1);
  });
});<|endoftext|>


<commit_before>/* eslint array-plural/array-plural: off, no-sync: off, max-len: off */

const qs = require('querystring');
const fs = require('fs');
const path = require('path');
const assert = require('assert');
const {JSDOM, VirtualConsole} = require('jsdom');
const {RTM_EVENTS: {MESSAGE}} = require('@slack/client');
const {mean, shuffle, range, random} = require('lodash');
const Cube = require('cubejs');
require('cubejs/lib/solve');

const getParity = (permutation) => {
	const N = permutation.length;
	let parity = 0;

	for (const i of range(N)) {
		for (const j of range(i)) {
			if (permutation[i] < permutation[j]) {
				parity = parity === 0 ? 1 : 0;
			}
		}
	}

	return parity;
};

const getRandomPermutation = (size, indices) => {
	const indicesPermutation = shuffle(range(indices.length));
	if (getParity(indicesPermutation) === 1) {
		[indicesPermutation[0], indicesPermutation[1]] = [indicesPermutation[1], indicesPermutation[0]];
	}

	const permutation = range(size);

	for (const [from, to] of indicesPermutation.entries()) {
		permutation[indices[to]] = indices[from];
	}
	return permutation;
};

// https://github.com/ldez/cubejs/blob/master/src/cube.coffee#L154
const getRandomOrientation = (size, indices, max) => {
	let orientationCount = 0;
	const orientation = Array(size).fill(0);

	for (const i of range(indices.length - 1)) {
		orientation[indices[i]] = random(max - 1);
		orientationCount += orientation[indices[i]];
	}

	orientation[indices[indices.length - 1]] = (max - orientationCount % max) % max;
	return orientation;
};

Cube.initSolver();

const virtualConsole = new VirtualConsole();
virtualConsole.sendTo(console);

const {window} = new JSDOM('

', {runScripts: 'outside-only', virtualConsole});

window.localStorage = {}; // dummy

for (const scriptPath of [
	'dist/js/jquery.min.js',
	'src/js/mathlib.js',
	'src/js/kernel.js',
	'src/js/tools.js',
	'src/js/tools/cross.js',
]) {
	const script = fs.readFileSync(path.resolve(__dirname, '../lib/cstimer', scriptPath)).toString();
	window.eval(script.replace(/['"]use strict['"];/, '').replace('solve: solve_cross,', 'solve: solve_cross, solve_xcross: solve_xcross,'));
}

module.exports = ({rtmClient: rtm, webClient: slack}) => {
	const state = {
		scrambles: [],
	};

	const getAttachment = (scramble, size, inverse) => ({
		title: scramble,
		title_link: `https://alg.cubing.net/?${qs.encode({
			alg: scramble.replace(/'/g, '-').replace(/ /g, '_'),
			view: 'playback',
		})}`,
		image_url: `http://roudai.net/visualcube/visualcube.php?${qs.encode({
			fmt: 'png',
			size,
			sch: inverse ? 'yogwrb' : 'wrgyob',
			alg: scramble.replace(/ /g, ''),
		})}`,
	});

	const getTimeText = (time) => time === Infinity ? 'DNF' : time.toFixed(2);

	const faceColors = ['#fefe00', '#ffffff', '#ffa100', '#ee0000', '#00d800', '#0000f2'];

	rtm.on(MESSAGE, async (message) => {
		if (message.channel !== process.env.CHANNEL_SANDBOX) {
			return;
		}

		if (!message.text) {
			return;
		}

		const {text} = message;

		if (text.startsWith('スクランブル') || text.startsWith('F2L') || text.startsWith('LL')) {
			const countMatch = text.slice(2).match(/\d+/);
			const count = countMatch ? Math.min(12, parseInt(countMatch[0])) : 1;

			const scrambles = Array(count).fill().map(() => {
				if (text.startsWith('スクランブル')) {
					return Cube.scramble();
				}

				if (text.startsWith('F2L')) {
					const cube = new Cube();
					cube.ep = getRandomPermutation(12, [0, 1, 2, 3, 8, 9, 10, 11]);
					cube.cp = getRandomPermutation(8, range(8));
					cube.eo = getRandomOrientation(12, [0, 1, 2, 3, 8, 9, 10, 11], 2);
					cube.co = getRandomOrientation(8, range(8), 3);
					return Cube.inverse(cube.solve());
				}

				{
					assert(text.startsWith('LL'));

					const cube = new Cube();
					cube.ep = getRandomPermutation(12, [0, 1, 2, 3]);
					cube.cp = getRandomPermutation(8, [0, 1, 2, 3]);
					cube.eo = getRandomOrientation(12, [0, 1, 2, 3], 2);
					cube.co = getRandomOrientation(8, [0, 1, 2, 3], 3);
					return Cube.inverse(cube.solve(18));
				}
			});
			state.scrambles = scrambles;

			await slack.chat.postMessage(process.env.CHANNEL_SANDBOX, '', {
				username: 'cubebot',
				icon_url: 'https://i.imgur.com/YyCc0mc.png',
				attachments: scrambles.map((scramble) => getAttachment(scramble, count > 1 ? 80 : 200, !text.startsWith('スクランブル'))),
			});
		}

		if (text === 'クロス') {
			for (const scramble of state.scrambles.slice(0, 5)) {
				const crosses = [];
				const xcrosses = [];

				const div = window.$('
');
				window.cross.solve(scramble, div);

				for (const index of [0, 1, 2, 3, 4, 5]) {
					const solve = div.children().eq(index).text().split(':')[1].trim();
					crosses.push(solve);
				}

				for (const index of [0, 1, 2, 3, 4, 5]) {
					const span = window.$('');
					window.cross.solve_xcross(window.kernel.parseScramble(scramble, 'FRUBLD'), index, span);
					const solve = span.text().split(':')[1].trim();
					xcrosses.push(solve);
				}

				await slack.chat.postMessage(process.env.CHANNEL_SANDBOX, '', {
					username: 'cubebot',
					icon_url: 'https://i.imgur.com/YyCc0mc.png',
					attachments: [
						getAttachment(scramble, 80),
						...faceColors.map((color, index) => {
							const xcross = xcrosses[index];
							const rotation = xcross.match(/^[xyz]2?'?/);

							return {
								color,
								text: [
									`cross: <https://alg.cubing.net/?${qs.encode({
										setup: (scramble + (rotation ? ` ${rotation[0]}` : '')).replace(/'/g, '-').replace(/ /g, '_'),
										alg: (crosses[index].replace(/^[xyz]2?'? /, '')).replace(/'/g, '-').replace(/ /g, '_'),
										view: 'playback',
									})}|${crosses[index]}>`,
									`x-cross: <https://alg.cubing.net/?${qs.encode({
										setup: (scramble + (rotation ? ` ${rotation[0]}` : '')).replace(/'/g, '-').replace(/ /g, '_'),
										alg: (xcrosses[index].replace(/^[xyz]2?'? /, '')).replace(/'/g, '-').replace(/ /g, '_'),
										view: 'playback',
									})}|${xcrosses[index]}>`,
								].join('\n'),
							};
						}),
					],
				});
			}
		}

		if (text.match(/^\s*(([\d.,]+|DNF)\s*)+$/i)) {
			const times = text.replace(/,/g, '.').split(/\s+/).filter((time) => time.length > 0).map((time) => parseFloat(time) || Infinity);

			if (times.length <= 1) {
				return;
			}

			if (times.length < 5) {
				const timeTexts = times.map((time) => getTimeText(time));

				slack.chat.postMessage(process.env.CHANNEL_SANDBOX, `*${getTimeText(mean(times))}*: ${timeTexts.join(' ')}`, {
					username: 'cubebot',
					icon_url: 'https://i.imgur.com/YyCc0mc.png',
					thread_ts: message.thread_ts,
				});
			} else {
				const maxIndex = times.indexOf(Math.max(...times));
				const minIndex = times.indexOf(Math.min(...times));
				const average = mean(times.filter((time, index) => index !== maxIndex && index !== minIndex));
				const fixedTimes = times.map((time, index) => (index === maxIndex || index === minIndex) ? `(${getTimeText(time)})` : getTimeText(time));

				slack.chat.postMessage(process.env.CHANNEL_SANDBOX, `*${getTimeText(average)}*: ${fixedTimes.join(' ')}`, {
					username: 'cubebot',
					icon_url: 'https://i.imgur.com/YyCc0mc.png',
					thread_ts: message.thread_ts,
				});
			}
		}
	});
};
<commit_msg>Implement PLL, ZBLL, CMLL, L6E<commit_after>/* eslint array-plural/array-plural: off, no-sync: off, max-len: off */

const qs = require('querystring');
const fs = require('fs');
const path = require('path');
const assert = require('assert');
const {JSDOM, VirtualConsole} = require('jsdom');
const {RTM_EVENTS: {MESSAGE}} = require('@slack/client');
const {mean, shuffle, range, random} = require('lodash');
const Cube = require('cubejs');
require('cubejs/lib/solve');

const getParity = (permutation) => {
	const N = permutation.length;
	let parity = 0;

	for (const i of range(N)) {
		for (const j of range(i)) {
			if (permutation[i] < permutation[j]) {
				parity = parity === 0 ? 1 : 0;
			}
		}
	}

	return parity;
};

const getRandomPermutation = (size, indices) => {
	const indicesPermutation = shuffle(range(indices.length));
	if (getParity(indicesPermutation) === 1) {
		[indicesPermutation[0], indicesPermutation[1]] = [indicesPermutation[1], indicesPermutation[0]];
	}

	const permutation = range(size);

	for (const [from, to] of indicesPermutation.entries()) {
		permutation[indices[to]] = indices[from];
	}
	return permutation;
};

// https://github.com/ldez/cubejs/blob/master/src/cube.coffee#L154
const getRandomOrientation = (size, indices, max) => {
	let orientationCount = 0;
	const orientation = Array(size).fill(0);

	for (const i of range(indices.length - 1)) {
		orientation[indices[i]] = random(max - 1);
		orientationCount += orientation[indices[i]];
	}

	orientation[indices[indices.length - 1]] = (max - orientationCount % max) % max;
	return orientation;
};

Cube.initSolver();

const virtualConsole = new VirtualConsole();
virtualConsole.sendTo(console);

const {window} = new JSDOM('

', {runScripts: 'outside-only', virtualConsole});

window.localStorage = {}; // dummy

for (const scriptPath of [
	'dist/js/jquery.min.js',
	'src/js/mathlib.js',
	'src/js/kernel.js',
	'src/js/tools.js',
	'src/js/tools/cross.js',
]) {
	const script = fs.readFileSync(path.resolve(__dirname, '../lib/cstimer', scriptPath)).toString();
	window.eval(script.replace(/['"]use strict['"];/, '').replace('solve: solve_cross,', 'solve: solve_cross, solve_xcross: solve_xcross,'));
}

module.exports = ({rtmClient: rtm, webClient: slack}) => {
	const state = {
		scrambles: [],
	};

	const getAttachment = (scramble, size, inverse) => ({
		title: scramble,
		title_link: `https://alg.cubing.net/?${qs.encode({
			alg: scramble.replace(/'/g, '-').replace(/ /g, '_'),
			view: 'playback',
		})}`,
		image_url: `http://roudai.net/visualcube/visualcube.php?${qs.encode({
			fmt: 'png',
			size,
			sch: inverse ? 'yogwrb' : 'wrgyob',
			alg: scramble.replace(/ /g, ''),
		})}`,
	});

	const getTimeText = (time) => time === Infinity ? 'DNF' : time.toFixed(2);

	const faceColors = ['#fefe00', '#ffffff', '#ffa100', '#ee0000', '#00d800', '#0000f2'];

	rtm.on(MESSAGE, async (message) => {
		if (message.channel !== process.env.CHANNEL_SANDBOX) {
			return;
		}

		if (!message.text) {
			return;
		}

		const {text} = message;
		let match = null;

		if ((match = text.match(/^(スクランブル|F2L|PLL|LL|ZBLL|CMLL|L6E)/))) {
			const countMatch = text.slice(match[1].length).match(/\d+/);
			const count = countMatch ? Math.min(12, parseInt(countMatch[0])) : 1;

			const scrambles = Array(count).fill().map(() => {
				if (text.startsWith('スクランブル')) {
					return Cube.scramble();
				}

				if (text.startsWith('F2L')) {
					const cube = new Cube();
					cube.ep = getRandomPermutation(12, [0, 1, 2, 3, 8, 9, 10, 11]);
					cube.cp = getRandomPermutation(8, range(8));
					cube.eo = getRandomOrientation(12, [0, 1, 2, 3, 8, 9, 10, 11], 2);
					cube.co = getRandomOrientation(8, range(8), 3);
					return Cube.inverse(cube.solve());
				}

				if (text.startsWith('LL')) {
					const cube = new Cube();
					cube.ep = getRandomPermutation(12, [0, 1, 2, 3]);
					cube.cp = getRandomPermutation(8, [0, 1, 2, 3]);
					cube.eo = getRandomOrientation(12, [0, 1, 2, 3], 2);
					cube.co = getRandomOrientation(8, [0, 1, 2, 3], 3);
					return Cube.inverse(cube.solve(20));
				}

				if (text.startsWith('PLL')) {
					const cube = new Cube();
					cube.ep = getRandomPermutation(12, [0, 1, 2, 3]);
					cube.cp = getRandomPermutation(8, [0, 1, 2, 3]);
					return Cube.inverse(cube.solve(20));
				}

				if (text.startsWith('ZBLL')) {
					const cube = new Cube();
					cube.ep = getRandomPermutation(12, [0, 1, 2, 3]);
					cube.cp = getRandomPermutation(8, [0, 1, 2, 3]);
					cube.co = getRandomOrientation(8, [0, 1, 2, 3], 3);
					return Cube.inverse(cube.solve(20));
				}

				if (text.startsWith('CMLL')) {
					const cube = new Cube();
					cube.ep = getRandomPermutation(12, [0, 1, 2, 3, 5, 7]);
					cube.cp = getRandomPermutation(8, [0, 1, 2, 3]);
					cube.eo = getRandomOrientation(12, [0, 1, 2, 3, 5, 7], 2);
					cube.co = getRandomOrientation(8, [0, 1, 2, 3], 3);
					return Cube.inverse(cube.solve(20));
				}

				{
					assert(text.startsWith('L6E'));

					const cube = new Cube();
					cube.ep = getRandomPermutation(12, [0, 1, 2, 3, 5, 7]);
					cube.eo = getRandomOrientation(12, [0, 1, 2, 3, 5, 7], 2);
					return Cube.inverse(cube.solve());
				}
			});
			state.scrambles = scrambles;

			await slack.chat.postMessage(process.env.CHANNEL_SANDBOX, '', {
				username: 'cubebot',
				icon_url: 'https://i.imgur.com/YyCc0mc.png',
				attachments: scrambles.map((scramble) => getAttachment(scramble, count > 1 ? 80 : 200, !text.startsWith('スクランブル'))),
			});
		}

		if (text === 'クロス') {
			for (const scramble of state.scrambles.slice(0, 5)) {
				const crosses = [];
				const xcrosses = [];

				const div = window.$('
');
				window.cross.solve(scramble, div);

				for (const index of [0, 1, 2, 3, 4, 5]) {
					const solve = div.children().eq(index).text().split(':')[1].trim();
					crosses.push(solve);
				}

				for (const index of [0, 1, 2, 3, 4, 5]) {
					const span = window.$('');
					window.cross.solve_xcross(window.kernel.parseScramble(scramble, 'FRUBLD'), index, span);
					const solve = span.text().split(':')[1].trim();
					xcrosses.push(solve);
				}

				await slack.chat.postMessage(process.env.CHANNEL_SANDBOX, '', {
					username: 'cubebot',
					icon_url: 'https://i.imgur.com/YyCc0mc.png',
					attachments: [
						getAttachment(scramble, 80),
						...faceColors.map((color, index) => {
							const xcross = xcrosses[index];
							const rotation = xcross.match(/^[xyz]2?'?/);

							return {
								color,
								text: [
									`cross: <https://alg.cubing.net/?${qs.encode({
										setup: (scramble + (rotation ? ` ${rotation[0]}` : '')).replace(/'/g, '-').replace(/ /g, '_'),
										alg: (crosses[index].replace(/^[xyz]2?'? /, '')).replace(/'/g, '-').replace(/ /g, '_'),
										view: 'playback',
									})}|${crosses[index]}>`,
									`x-cross: <https://alg.cubing.net/?${qs.encode({
										setup: (scramble + (rotation ? ` ${rotation[0]}` : '')).replace(/'/g, '-').replace(/ /g, '_'),
										alg: (xcrosses[index].replace(/^[xyz]2?'? /, '')).replace(/'/g, '-').replace(/ /g, '_'),
										view: 'playback',
									})}|${xcrosses[index]}>`,
								].join('\n'),
							};
						}),
					],
				});
			}
		}

		if (text.match(/^\s*(([\d.,]+|DNF)\s*)+$/i)) {
			const times = text.replace(/,/g, '.').split(/\s+/).filter((time) => time.length > 0).map((time) => parseFloat(time) || Infinity);

			if (times.length <= 1) {
				return;
			}

			if (times.length < 5) {
				const timeTexts = times.map((time) => getTimeText(time));

				slack.chat.postMessage(process.env.CHANNEL_SANDBOX, `*${getTimeText(mean(times))}*: ${timeTexts.join(' ')}`, {
					username: 'cubebot',
					icon_url: 'https://i.imgur.com/YyCc0mc.png',
					thread_ts: message.thread_ts,
				});
			} else {
				const maxIndex = times.indexOf(Math.max(...times));
				const minIndex = times.indexOf(Math.min(...times));
				const average = mean(times.filter((time, index) => index !== maxIndex && index !== minIndex));
				const fixedTimes = times.map((time, index) => (index === maxIndex || index === minIndex) ? `(${getTimeText(time)})` : getTimeText(time));

				slack.chat.postMessage(process.env.CHANNEL_SANDBOX, `*${getTimeText(average)}*: ${fixedTimes.join(' ')}`, {
					username: 'cubebot',
					icon_url: 'https://i.imgur.com/YyCc0mc.png',
					thread_ts: message.thread_ts,
				});
			}
		}
	});
};
<|endoftext|>