//
// Forms
// --------------------------------------------------

// GENERAL STYLES
// --------------

// Make all forms have space below them
form {
  margin: 0 0 @baseLineHeight;
}

fieldset {
  padding: 0;
  margin: 0;
  border: 0;
}

// Groups of fields with labels on top (legends)
legend {
  display: block;
  width: 100%;
  padding: 0;
  margin-bottom: @baseLineHeight;
  font-size: @baseFontSize * 1.5;
  line-height: @baseLineHeight * 2;
  color: @grayDark;
  border: 0;
  border-bottom: 1px solid #e5e5e5;

  // Small
  small {
    font-size: @baseLineHeight * .75;
    color: @grayLight;
  }
}

// Set font for forms
label,
input,
button,
select,
textarea {
  #font > .shorthand(@baseFontSize,normal,@baseLineHeight); // Set size, weight, line-height here
}
input,
button,
select,
textarea {
  font-family: @baseFontFamily; // And only set font-family here for those that need it (note the missing label element)
}

// Identify controls by their labels
label {
  display: block;
  margin-bottom: 5px;
}

// Form controls
// -------------------------

// Shared size and type resets
select,
textarea,
input[type="text"],
input[type="password"],
input[type="datetime"],
input[type="datetime-local"],
input[type="date"],
input[type="month"],
input[type="time"],
input[type="week"],
input[type="number"],
input[type="email"],
input[type="url"],
input[type="search"],
input[type="tel"],
input[type="color"],
.uneditable-input {
  display: inline-block;
  height: @baseLineHeight;
  padding: 4px 6px;
  margin-bottom: @baseLineHeight / 2;
  font-size: @baseFontSize;
  line-height: @baseLineHeight;
  color: @gray;
  .border-radius(@inputBorderRadius);
  vertical-align: middle;
}

// Reset appearance properties for textual inputs and textarea
// Declare width for legacy (can't be on input[type=*] selectors or it's too specific)
input,
textarea,
.uneditable-input {
  width: 206px; // plus 12px padding and 2px border
}
// Reset height since textareas have rows
textarea {
  height: auto;
}
// Everything else
textarea,
input[type="text"],
input[type="password"],
input[type="datetime"],
input[type="datetime-local"],
input[type="date"],
input[type="month"],
input[type="time"],
input[type="week"],
input[type="number"],
input[type="email"],
input[type="url"],
input[type="search"],
input[type="tel"],
input[type="color"],
.uneditable-input {
  background-color: @inputBackground;
  border: 1px solid @inputBorder;
  .box-shadow(inset 0 1px 1px rgba(0,0,0,.075));
  .transition(~"border linear .2s, box-shadow linear .2s");

  // Focus state
  &:focus {
    border-color: rgba(82,168,236,.8);
    outline: 0;
    outline: thin dotted \9; /* IE6-9 */
    .box-shadow(~"inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(82,168,236,.6)");
  }
}

// Position radios and checkboxes better
input[type="radio"],
input[type="checkbox"] {
  margin: 4px 0 0;
  *margin-top: 0; /* IE7 */
  margin-top: 1px \9; /* IE8-9 */
  line-height: normal;
}

// Reset width of input images, buttons, radios, checkboxes
input[type="file"],
input[type="image"],
input[type="submit"],
input[type="reset"],
input[type="button"],
input[type="radio"],
input[type="checkbox"] {
  width: auto; // Override of generic input selector
}

// Set the height of select and file controls to match text inputs
select,
input[type="file"] {
  height: @inputHeight; /* In IE7, the height of the select element cannot be changed by height, only font-size */
  *margin-top: 4px; /* For IE7, add top margin to align select with labels */
  line-height: @inputHeight;
}

// Make select elements obey height by applying a border
select {
  width: 220px; // default input width + 10px of padding that doesn't get applied
  border: 1px solid @inputBorder;
  background-color: @inputBackground; // Chrome on Linux and Mobile Safari need background-color
}

// Make multiple select elements height not fixed
select[multiple],
select[size] {
  height: auto;
}

// Focus for select, file, radio, and checkbox
select:focus,
input[type="file"]:focus,
input[type="radio"]:focus,
input[type="checkbox"]:focus {
  .tab-focus();
}

// Uneditable inputs
// -------------------------

// Make uneditable inputs look inactive
.uneditable-input,
.uneditable-textarea {
  color: @grayLight;
  background-color: darken(@inputBackground, 1%);
  border-color: @inputBorder;
  .box-shadow(inset 0 1px 2px rgba(0,0,0,.025));
  cursor: not-allowed;
}

// For text that needs to appear as an input but should not be an input
.uneditable-input {
  overflow: hidden; // prevent text from wrapping, but still cut it off like an input does
  white-space: nowrap;
}

// Make uneditable textareas behave like a textarea
.uneditable-textarea {
  width: auto;
  height: auto;
}

// Placeholder
// -------------------------

// Placeholder text gets special styles because when browsers invalidate entire lines if it doesn't understand a selector
input,
textarea {
  .placeholder();
}

// CHECKBOXES & RADIOS
// -------------------

// Indent the labels to position radios/checkboxes as hanging
.radio,
.checkbox {
  min-height: @baseLineHeight; // clear the floating input if there is no label text
  padding-left: 20px;
}
.radio input[type="radio"],
.checkbox input[type="checkbox"] {
  float: left;
  margin-left: -20px;
}

// Move the options list down to align with labels
.controls > .radio:first-child,
.controls > .checkbox:first-child {
  padding-top: 5px; // has to be padding because margin collaspes
}

// Radios and checkboxes on same line
// TODO v3: Convert .inline to .control-inline
.radio.inline,
.checkbox.inline {
  display: inline-block;
  padding-top: 5px;
  margin-bottom: 0;
  vertical-align: middle;
}
.radio.inline + .radio.inline,
.checkbox.inline + .checkbox.inline {
  margin-left: 10px; // space out consecutive inline controls
}

// INPUT SIZES
// -----------

// General classes for quick sizes
.input-mini       { width: 60px; }
.input-small      { width: 90px; }
.input-medium     { width: 150px; }
.input-large      { width: 210px; }
.input-xlarge     { width: 270px; }
.input-xxlarge    { width: 530px; }

// Grid style input sizes
input[class*="span"],
select[class*="span"],
textarea[class*="span"],
.uneditable-input[class*="span"],
// Redeclare since the fluid row class is more specific
.row-fluid input[class*="span"],
.row-fluid select[class*="span"],
.row-fluid textarea[class*="span"],
.row-fluid .uneditable-input[class*="span"] {
  float: none;
  margin-left: 0;
}
// Ensure input-prepend/append never wraps
.input-append input[class*="span"],
.input-append .uneditable-input[class*="span"],
.input-prepend input[class*="span"],
.input-prepend .uneditable-input[class*="span"],
.row-fluid input[class*="span"],
.row-fluid select[class*="span"],
.row-fluid textarea[class*="span"],
.row-fluid .uneditable-input[class*="span"],
.row-fluid .input-prepend [class*="span"],
.row-fluid .input-append [class*="span"] {
  display: inline-block;
}

// GRID SIZING FOR INPUTS
// ----------------------

// Grid sizes
#grid > .input(@gridColumnWidth, @gridGutterWidth);

// Control row for multiple inputs per line
.controls-row {
  .clearfix(); // Clear the float from controls
}

// Float to collapse white-space for proper grid alignment
.controls-row [class*="span"],
// Redeclare the fluid grid collapse since we undo the float for inputs
.row-fluid .controls-row [class*="span"] {
  float: left;
}
// Explicity set top padding on all checkboxes/radios, not just first-child
.controls-row .checkbox[class*="span"],
.controls-row .radio[class*="span"] {
  padding-top: 5px;
}

// DISABLED STATE
// --------------

// Disabled and read-only inputs
input[disabled],
select[disabled],
textarea[disabled],
input[readonly],
select[readonly],
textarea[readonly] {
  cursor: not-allowed;
  background-color: @inputDisabledBackground;
}
// Explicitly reset the colors here
input[type="radio"][disabled],
input[type="checkbox"][disabled],
input[type="radio"][readonly],
input[type="checkbox"][readonly] {
  background-color: transparent;
}

// FORM FIELD FEEDBACK STATES
// --------------------------

// Warning
.control-group.warning {
  .formFieldState(@warningText, @warningText, @warningBackground);
}
// Error
.control-group.error {
  .formFieldState(@errorText, @errorText, @errorBackground);
}
// Success
.control-group.success {
  .formFieldState(@successText, @successText, @successBackground);
}
// Success
.control-group.info {
  .formFieldState(@infoText, @infoText, @infoBackground);
}

// HTML5 invalid states
// Shares styles with the .control-group.error above
input:focus:invalid,
textarea:focus:invalid,
select:focus:invalid {
  color: #b94a48;
  border-color: #ee5f5b;
  &:focus {
    border-color: darken(#ee5f5b, 10%);
    @shadow: 0 0 6px lighten(#ee5f5b, 20%);
    .box-shadow(@shadow);
  }
}

// FORM ACTIONS
// ------------

.form-actions {
  padding: (@baseLineHeight - 1) 20px @baseLineHeight;
  margin-top: @baseLineHeight;
  margin-bottom: @baseLineHeight;
  background-color: @formActionsBackground;
  border-top: 1px solid #e5e5e5;
  .clearfix(); // Adding clearfix to allow for .pull-right button containers
}

// HELP TEXT
// ---------

.help-block,
.help-inline {
  color: lighten(@textColor, 15%); // lighten the text some for contrast
}

.help-block {
  display: block; // account for any element using help-block
  margin-bottom: @baseLineHeight / 2;
}

.help-inline {
  display: inline-block;
  .ie7-inline-block();
  vertical-align: middle;
  padding-left: 5px;
}

// INPUT GROUPS
// ------------

// Allow us to put symbols and text within the input field for a cleaner look
.input-append,
.input-prepend {
  display: inline-block;
  margin-bottom: @baseLineHeight / 2;
  vertical-align: middle;
  font-size: 0; // white space collapse hack
  white-space: nowrap; // Prevent span and input from separating

  // Reset the white space collapse hack
  input,
  select,
  .uneditable-input,
  .dropdown-menu,
  .popover {
    font-size: @baseFontSize;
  }

  input,
  select,
  .uneditable-input {
    position: relative; // placed here by default so that on :focus we can place the input above the .add-on for full border and box-shadow goodness
    margin-bottom: 0; // prevent bottom margin from screwing up alignment in stacked forms
    *margin-left: 0;
    vertical-align: top;
    .border-radius(0 @inputBorderRadius @inputBorderRadius 0);
    // Make input on top when focused so blue border and shadow always show
    &:focus {
      z-index: 2;
    }
  }
  .add-on {
    display: inline-block;
    width: auto;
    height: @baseLineHeight;
    min-width: 16px;
    padding: 4px 5px;
    font-size: @baseFontSize;
    font-weight: normal;
    line-height: @baseLineHeight;
    text-align: center;
    text-shadow: 0 1px 0 @white;
    background-color: @grayLighter;
    border: 1px solid #ccc;
  }
  .add-on,
  .btn,
  .btn-group > .dropdown-toggle {
    vertical-align: top;
    .border-radius(0);
  }
  .active {
    background-color: lighten(@green, 30);
    border-color: @green;
  }
}

.input-prepend {
  .add-on,
  .btn {
    margin-right: -1px;
  }
  .add-on:first-child,
  .btn:first-child {
    // FYI, `.btn:first-child` accounts for a button group that's prepended
    .border-radius(@inputBorderRadius 0 0 @inputBorderRadius);
  }
}

.input-append {
  input,
  select,
  .uneditable-input {
    .border-radius(@inputBorderRadius 0 0 @inputBorderRadius);
    + .btn-group .btn:last-child {
      .border-radius(0 @inputBorderRadius @inputBorderRadius 0);
    }
  }
  .add-on,
  .btn,
  .btn-group {
    margin-left: -1px;
  }
  .add-on:last-child,
  .btn:last-child,
  .btn-group:last-child > .dropdown-toggle {
    .border-radius(0 @inputBorderRadius @inputBorderRadius 0);
  }
}

// Remove all border-radius for inputs with both prepend and append
.input-prepend.input-append {
  input,
  select,
  .uneditable-input {
    .border-radius(0);
    + .btn-group .btn {
      .border-radius(0 @inputBorderRadius @inputBorderRadius 0);
    }
  }
  .add-on:first-child,
  .btn:first-child {
    margin-right: -1px;
    .border-radius(@inputBorderRadius 0 0 @inputBorderRadius);
  }
  .add-on:last-child,
  .btn:last-child {
    margin-left: -1px;
    .border-radius(0 @inputBorderRadius @inputBorderRadius 0);
  }
  .btn-group:first-child {
    margin-left: 0;
  }
}

// SEARCH FORM
// -----------

input.search-query {
  padding-right: 14px;
  padding-right: 4px \9;
  padding-left: 14px;
  padding-left: 4px \9; /* IE7-8 doesn't have border-radius, so don't indent the padding */
  margin-bottom: 0; // Remove the default margin on all inputs
  .border-radius(15px);
}

/* Allow for input prepend/append in search forms */
.form-search .input-append .search-query,
.form-search .input-prepend .search-query {
  .border-radius(0); // Override due to specificity
}
.form-search .input-append .search-query {
  .border-radius(14px 0 0 14px);
}
.form-search .input-append .btn {
  .border-radius(0 14px 14px 0);
}
.form-search .input-prepend .search-query {
  .border-radius(0 14px 14px 0);
}
.form-search .input-prepend .btn {
  .border-radius(14px 0 0 14px);
}

// HORIZONTAL & VERTICAL FORMS
// ---------------------------

// Common properties
// -----------------

.form-search,
.form-inline,
.form-horizontal {
  input,
  textarea,
  select,
  .help-inline,
  .uneditable-input,
  .input-prepend,
  .input-append {
    display: inline-block;
    .ie7-inline-block();
    margin-bottom: 0;
    vertical-align: middle;
  }
  // Re-hide hidden elements due to specifity
  .hide {
    display: none;
  }
}
.form-search label,
.form-inline label,
.form-search .btn-group,
.form-inline .btn-group {
  display: inline-block;
}
// Remove margin for input-prepend/-append
.form-search .input-append,
.form-inline .input-append,
.form-search .input-prepend,
.form-inline .input-prepend {
  margin-bottom: 0;
}
// Inline checkbox/radio labels (remove padding on left)
.form-search .radio,
.form-search .checkbox,
.form-inline .radio,
.form-inline .checkbox {
  padding-left: 0;
  margin-bottom: 0;
  vertical-align: middle;
}
// Remove float and margin, set to inline-block
.form-search .radio input[type="radio"],
.form-search .checkbox input[type="checkbox"],
.form-inline .radio input[type="radio"],
.form-inline .checkbox input[type="checkbox"] {
  float: left;
  margin-right: 3px;
  margin-left: 0;
}

// Margin to space out fieldsets
.control-group {
  margin-bottom: @baseLineHeight / 2;
}

// Legend collapses margin, so next element is responsible for spacing
legend + .control-group {
  margin-top: @baseLineHeight;
  -webkit-margin-top-collapse: separate;
}

// Horizontal-specific styles
// --------------------------

.form-horizontal {
  // Increase spacing between groups
  .control-group {
    margin-bottom: @baseLineHeight;
    .clearfix();
  }
  // Float the labels left
  .control-label {
    float: left;
    width: @horizontalComponentOffset - 20;
    padding-top: 5px;
    text-align: right;
  }
  // Move over all input controls and content
  .controls {
    // Super jank IE7 fix to ensure the inputs in .input-append and input-prepend
    // don't inherit the margin of the parent, in this case .controls
    *display: inline-block;
    *padding-left: 20px;
    margin-left: @horizontalComponentOffset;
    *margin-left: 0;
    &:first-child {
      *padding-left: @horizontalComponentOffset;
    }
  }
  // Remove bottom margin on block level help text since that's accounted for on .control-group
  .help-block {
    margin-bottom: 0;
  }
  // And apply it only to .help-block instances that follow a form control
  input,
  select,
  textarea,
  .uneditable-input,
  .input-prepend,
  .input-append {
    + .help-block {
      margin-top: @baseLineHeight / 2;
    }
  }
  // Move over buttons in .form-actions to align with .controls
  .form-actions {
    padding-left: @horizontalComponentOffset;
  }
}


'use strict';

/**
 * Module dependencies.
 */

const isGeneratorFunction = require('is-generator-function');
const debug = require('debug')('koa:application');
const onFinished = require('on-finished');
const response = require('./response');
const compose = require('koa-compose');
const isJSON = require('koa-is-json');
const context = require('./context');
const request = require('./request');
const statuses = require('statuses');
const Emitter = require('events');
const util = require('util');
const Stream = require('stream');
const http = require('http');
const only = require('only');
const convert = require('koa-convert');
const deprecate = require('depd')('koa');

/**
 * Expose `Application` class.
 * Inherits from `Emitter.prototype`.
 */

module.exports = class Application extends Emitter {
  /**
   * Initialize a new `Application`.
   *
   * @api public
   */

  constructor() {
    super();

    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }

  /**
   * Shorthand for:
   *
   *    http.createServer(app.callback()).listen(...)
   *
   * @param {Mixed} ...
   * @return {Server}
   * @api public
   */

  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }

  /**
   * Return JSON representation.
   * We only bother showing settings.
   *
   * @return {Object}
   * @api public
   */

  toJSON() {
    return only(this, [
      'subdomainOffset',
      'proxy',
      'env'
    ]);
  }

  /**
   * Inspect implementation.
   *
   * @return {Object}
   * @api public
   */

  inspect() {
    return this.toJSON();
  }

  /**
   * Use the given middleware `fn`.
   *
   * Old-style middleware will be converted.
   *
   * @param {Function} fn
   * @return {Application} self
   * @api public
   */

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will be removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/blob/master/docs/migration.md');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

  /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */

  callback() {
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

  /**
   * Handle request in callback.
   *
   * @api private
   */

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

  /**
   * Initialize a new context.
   *
   * @api private
   */

  createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.state = {};
    return context;
  }

  /**
   * Default error handler.
   *
   * @param {Error} err
   * @api private
   */

  onerror(err) {
    if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));

    if (404 == err.status || err.expose) return;
    if (this.silent) return;

    const msg = err.stack || err.toString();
    console.error();
    console.error(msg.replace(/^/gm, '  '));
    console.error();
  }
};

/**
 * Response helper.
 */

function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;

  const res = ctx.res;
  if (!ctx.writable) return;

  let body = ctx.body;
  const code = ctx.status;

  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  if ('HEAD' == ctx.method) {
    if (!res.headersSent && isJSON(body)) {
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }

  // status body
  if (null == body) {
    body = ctx.message || String(code);
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }

  // responses
  if (Buffer.isBuffer(body)) return res.end(body);
  if ('string' == typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);

  // body: json
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}


'use strict';

const assert = require('assert');
const request = require('../helpers/context').request;

describe('req.headers', () => {
  it('should return the request header object', () => {
    const req = request();
    assert.deepEqual(req.headers, req.req.headers);
  });

  it('should set the request header object', () => {
    const req = request();
    req.headers = {'X-Custom-Headerfield': 'Its one header, with headerfields'};
    assert.deepEqual(req.headers, req.req.headers);
  });
});


'use strict';

const request = require('supertest');
const assert = require('assert');
const Koa = require('../..');

describe('app.response', () => {
  const app1 = new Koa();
  app1.response.msg = 'hello';
  const app2 = new Koa();

  it('should merge properties', () => {
    app1.use((ctx, next) => {
      assert.equal(ctx.response.msg, 'hello');
      ctx.status = 204;
    });

    return request(app1.listen())
      .get('/')
      .expect(204);
  });

  it('should not affect the original prototype', () => {
    app2.use((ctx, next) => {
      assert.equal(ctx.response.msg, undefined);
      ctx.status = 204;
    });

    return request(app2.listen())
      .get('/')
      .expect(204);
  });
});


'use strict';

const assert = require('assert');
const response = require('../helpers/context').response;

describe('res.etag=', () => {
  it('should not modify an etag with quotes', () => {
    const res = response();
    res.etag = '"asdf"';
    assert.equal(res.header.etag, '"asdf"');
  });

  it('should not modify a weak etag', () => {
    const res = response();
    res.etag = 'W/"asdf"';
    assert.equal(res.header.etag, 'W/"asdf"');
  });

  it('should add quotes around an etag if necessary', () => {
    const res = response();
    res.etag = 'asdf';
    assert.equal(res.header.etag, '"asdf"');
  });
});

describe('res.etag', () => {
  it('should return etag', () => {
    const res = response();
    res.etag = '"asdf"';
    assert.equal(res.etag, '"asdf"');
  });
});


'use strict';

const assert = require('assert');
const request = require('supertest');
const Koa = require('../..');

describe('ctx.cookies', () => {
  describe('ctx.cookies.set()', () => {
    it('should set an unsigned cookie', async () => {
      const app = new Koa();

      app.use((ctx, next) => {
        ctx.cookies.set('name', 'jon');
        ctx.status = 204;
      });

      const server = app.listen();

      const res = await request(server)
        .get('/')
        .expect(204);

      const cookie = res.headers['set-cookie'].some(cookie => /^name=/.test(cookie));
      assert.equal(cookie, true);
    });

    describe('with .signed', () => {
      describe('when no .keys are set', () => {
        it('should error', () => {
          const app = new Koa();

          app.use((ctx, next) => {
            try {
              ctx.cookies.set('foo', 'bar', { signed: true });
            } catch (err) {
              ctx.body = err.message;
            }
          });

          return request(app.callback())
            .get('/')
            .expect('.keys required for signed cookies');
        });
      });

      it('should send a signed cookie', async () => {
        const app = new Koa();

        app.keys = ['a', 'b'];

        app.use((ctx, next) => {
          ctx.cookies.set('name', 'jon', { signed: true });
          ctx.status = 204;
        });

        const server = app.listen();

        const res = await request(server)
          .get('/')
          .expect(204);

        const cookies = res.headers['set-cookie'];

        assert.equal(cookies.some(cookie => /^name=/.test(cookie)), true);
        assert.equal(cookies.some(cookie => /(,|^)name\.sig=/.test(cookie)), true);
      });
    });

    describe('with secure', () => {
      it('should get secure from request', async () => {
        const app = new Koa();

        app.proxy = true;
        app.keys = ['a', 'b'];

        app.use(ctx => {
          ctx.cookies.set('name', 'jon', { signed: true });
          ctx.status = 204;
        });

        const server = app.listen();

        const res = await request(server)
          .get('/')
          .set('x-forwarded-proto', 'https') // mock secure
          .expect(204);

        const cookies = res.headers['set-cookie'];
        assert.equal(cookies.some(cookie => /^name=/.test(cookie)), true);
        assert.equal(cookies.some(cookie => /(,|^)name\.sig=/.test(cookie)), true);
        assert.equal(cookies.every(cookie => /secure/.test(cookie)), true);
      });
    });
  });

  describe('ctx.cookies=', () => {
    it('should override cookie work', async () => {
      const app = new Koa();

      app.use((ctx, next) => {
        ctx.cookies = {
          set(key, value){
            ctx.set(key, value);
          }
        };
        ctx.cookies.set('name', 'jon');
        ctx.status = 204;
      });

      const server = app.listen();

      await request(server)
        .get('/')
        .expect('name', 'jon')
        .expect(204);
    });
  });
});


return request(app2.listen())
.get('/')
.expect(204);
});
});

it('should not include status message in body for http2', async () => {

const response = await request(app3.listen())


'use strict';

const context = require('../helpers/context');
const assert = require('assert');

describe('ctx.type=', () => {
  describe('with a mime', () => {
    it('should set the Content-Type', () => {
      const ctx = context();
      ctx.type = 'text/plain';
      assert.equal(ctx.type, 'text/plain');
      assert.equal(ctx.response.header['content-type'], 'text/plain; charset=utf-8');
    });
  });

  describe('with an extension', () => {
    it('should lookup the mime', () => {
      const ctx = context();
      ctx.type = 'json';
      assert.equal(ctx.type, 'application/json');
      assert.equal(ctx.response.header['content-type'], 'application/json; charset=utf-8');
    });
  });

  describe('without a charset', () => {
    it('should default the charset', () => {
      const ctx = context();
      ctx.type = 'text/html';
      assert.equal(ctx.type, 'text/html');
      assert.equal(ctx.response.header['content-type'], 'text/html; charset=utf-8');
    });
  });

  describe('with a charset', () => {
    it('should not default the charset', () => {
      const ctx = context();
      ctx.type = 'text/html; charset=foo';
      assert.equal(ctx.type, 'text/html');
      assert.equal(ctx.response.header['content-type'], 'text/html; charset=foo');
    });
  });

  describe('with an unknown extension', () => {
    it('should not set a content-type', () => {
      const ctx = context();
      ctx.type = 'asdf';
      assert(!ctx.type);
      assert(!ctx.response.header['content-type']);
    });
  });
});

describe('ctx.type', () => {
  describe('with no Content-Type', () => {
    it('should return ""', () => {
      const ctx = context();
      assert(!ctx.type);
    });
  });

  describe('with a Content-Type', () => {
    it('should return the mime', () => {
      const ctx = context();
      ctx.type = 'json';
      assert.equal(ctx.type, 'application/json');
    });
  });
});


'use strict';

const assert = require('assert');
const context = require('../helpers/context');

describe('ctx.get(name)', () => {
  it('should return the field value', () => {
    const ctx = context();
    ctx.req.headers.host = 'http://google.com';
    ctx.req.headers.referer = 'http://google.com';
    assert.equal(ctx.get('HOST'), 'http://google.com');
    assert.equal(ctx.get('Host'), 'http://google.com');
    assert.equal(ctx.get('host'), 'http://google.com');
    assert.equal(ctx.get('referer'), 'http://google.com');
    assert.equal(ctx.get('referrer'), 'http://google.com');
  });
});


'use strict';

const assert = require('assert');
const context = require('../helpers/context');

describe('ctx.set(name, val)', () => {
  it('should set a field value', () => {
    const ctx = context();
    ctx.set('x-foo', 'bar');
    assert.equal(ctx.response.header['x-foo'], 'bar');
  });

  it('should coerce number to string', () => {
    const ctx = context();
    ctx.set('x-foo', 5);
    assert.equal(ctx.response.header['x-foo'], '5');
  });

  it('should coerce undefined to string', () => {
    const ctx = context();
    ctx.set('x-foo', undefined);
    assert.equal(ctx.response.header['x-foo'], 'undefined');
  });

  it('should set a field value of array', () => {
    const ctx = context();
    ctx.set('x-foo', ['foo', 'bar']);
    assert.deepEqual(ctx.response.header['x-foo'], [ 'foo', 'bar' ]);
  });
});

describe('ctx.set(object)', () => {
  it('should set multiple fields', () => {
    const ctx = context();

    ctx.set({
      foo: '1',
      bar: '2'
    });

    assert.equal(ctx.response.header.foo, '1');
    assert.equal(ctx.response.header.bar, '2');
  });
});


var first = e.touches ? e.touches[0] : e;

this._startPoint = new L.Point(first.clientX, first.clientY);

this._startPos = this._newPos = L.DomUtil.getPosition(this._element);

/*
 * L.Handler.MapDrag is used to make the map draggable (with panning inertia), enabled by default.
 */

L.Map.mergeOptions({
	dragging: true,

	inertia: !L.Browser.android23,
	inertiaDeceleration: 3400, // px/s^2
	inertiaMaxSpeed: Infinity, // px/s
	inertiaThreshold: L.Browser.touch ? 32 : 18, // ms
	easeLinearity: 0.25,

	// TODO refactor, move to CRS
	worldCopyJump: false
});

L.Map.Drag = L.Handler.extend({
	addHooks: function () {
		if (!this._draggable) {
			var map = this._map;

			this._draggable = new L.Draggable(map._mapPane, map._container);

			this._draggable.on({
				dragstart: this._onDragStart,
				drag: this._onDrag,
				dragend: this._onDragEnd
			}, this);

			if (map.options.worldCopyJump) {
				this._draggable.on('predrag', this._onPreDrag, this);
				map.on('viewreset', this._onViewReset, this);

				map.whenReady(this._onViewReset, this);
			}
		}
		this._draggable.enable();
	},

	removeHooks: function () {
		this._draggable.disable();
	},

	moved: function () {
		return this._draggable && this._draggable._moved;
	},

	_onDragStart: function () {
		var map = this._map;

		if (map._panAnim) {
			map._panAnim.stop();
		}

		map
		    .fire('movestart')
		    .fire('dragstart');

		if (map.options.inertia) {
			this._positions = [];
			this._times = [];
		}
	},

	_onDrag: function () {
		if (this._map.options.inertia) {
			var time = this._lastTime = +new Date(),
			    pos = this._lastPos = this._draggable._newPos;

			this._positions.push(pos);
			this._times.push(time);

			if (time - this._times[0] > 200) {
				this._positions.shift();
				this._times.shift();
			}
		}

		this._map
		    .fire('move')
		    .fire('drag');
	},

	_onViewReset: function () {
		var pxCenter = this._map.getSize().divideBy(2),
		    pxWorldCenter = this._map.latLngToLayerPoint([0, 0]);

		this._initialWorldOffset = pxWorldCenter.subtract(pxCenter).x;
		this._worldWidth = this._map.getPixelWorldBounds().getSize().x;
	},

	_onPreDrag: function () {
		// TODO refactor to be able to adjust map pane position after zoom
		var worldWidth = this._worldWidth,
		    halfWidth = Math.round(worldWidth / 2),
		    dx = this._initialWorldOffset,
		    x = this._draggable._newPos.x,
		    newX1 = (x - halfWidth + dx) % worldWidth + halfWidth - dx,
		    newX2 = (x + halfWidth + dx) % worldWidth - halfWidth - dx,
		    newX = Math.abs(newX1 + dx) < Math.abs(newX2 + dx) ? newX1 : newX2;

		this._draggable._newPos.x = newX;
	},

	_onDragEnd: function (e) {
		var map = this._map,
		    options = map.options,
		    delay = +new Date() - this._lastTime,

		    noInertia = !options.inertia || delay > options.inertiaThreshold || !this._positions[0];

		map.fire('dragend', e);

		if (noInertia) {
			map.fire('moveend');

		} else {

			var direction = this._lastPos.subtract(this._positions[0]),
			    duration = (this._lastTime + delay - this._times[0]) / 1000,
			    ease = options.easeLinearity,

			    speedVector = direction.multiplyBy(ease / duration),
			    speed = speedVector.distanceTo([0, 0]),

			    limitedSpeed = Math.min(options.inertiaMaxSpeed, speed),
			    limitedSpeedVector = speedVector.multiplyBy(limitedSpeed / speed),

			    decelerationDuration = limitedSpeed / (options.inertiaDeceleration * ease),
			    offset = limitedSpeedVector.multiplyBy(-decelerationDuration / 2).round();

			if (!offset.x || !offset.y) {
				map.fire('moveend');

			} else {
				offset = map._limitOffset(offset, map.options.maxBounds);

				L.Util.requestAnimFrame(function () {
					map.panBy(offset, {
						duration: decelerationDuration,
						easeLinearity: ease,
						noMoveStart: true
					});
				});
			}
		}
	}
});

L.Map.addInitHook('addHandler', 'dragging', L.Map.Drag);


describe('Popup', function () {

	var c, map;

	beforeEach(function () {
		c = document.createElement('div');
		c.style.width = '400px';
		c.style.height = '400px';
		map = new L.Map(c);
		map.setView(new L.LatLng(55.8, 37.6), 6);
	});

	it("closes on map click when map has closePopupOnClick option", function () {
		map.options.closePopupOnClick = true;

		var popup = new L.Popup()
			.setLatLng(new L.LatLng(55.8, 37.6))
			.openOn(map);

		happen.click(c);

		expect(map.hasLayer(popup)).to.be(false);
	});

	it("closes on map click when popup has closeOnClick option", function () {
		map.options.closePopupOnClick = false;

		var popup = new L.Popup({closeOnClick: true})
			.setLatLng(new L.LatLng(55.8, 37.6))
			.openOn(map);

		happen.click(c);

		expect(map.hasLayer(popup)).to.be(false);
	});

	it("does not close on map click when popup has closeOnClick: false option", function () {
		map.options.closePopupOnClick = true;

		var popup = new L.Popup({closeOnClick: false})
			.setLatLng(new L.LatLng(55.8, 37.6))
			.openOn(map);

		happen.click(c);

		expect(map.hasLayer(popup)).to.be(true);
	});

	it("toggles its visibility when marker is clicked", function () {
		var marker = new L.Marker(new L.LatLng(55.8, 37.6));
		map.addLayer(marker);

		marker.bindPopup('Popup1').openPopup();

		map.options.closePopupOnClick = true;
		happen.click(c);

		// toggle open popup
		sinon.spy(marker, "openPopup");
		marker.fire('click');
		expect(marker.openPopup.calledOnce).to.be(true);
		expect(map.hasLayer(marker._popup)).to.be(true);
		marker.openPopup.restore();

		// toggle close popup
		sinon.spy(marker, "closePopup");
		marker.fire('click');
		expect(marker.closePopup.calledOnce).to.be(true);
		expect(map.hasLayer(marker._popup)).to.be(false);
		marker.closePopup.restore();
	});

	it("should trigger popupopen on marker when popup opens", function () {
		var marker1 = new L.Marker(new L.LatLng(55.8, 37.6));
		var marker2 = new L.Marker(new L.LatLng(57.123076977278, 44.861962891635));

		map.addLayer(marker1);
		map.addLayer(marker2);

		marker1.bindPopup('Popup1');
		marker2.bindPopup('Popup2');

		var spy = sinon.spy();

		marker1.on('popupopen', spy);

		expect(spy.called).to.be(false);
		marker2.openPopup();
		expect(spy.called).to.be(false);
		marker1.openPopup();
		expect(spy.called).to.be(true);
	});

	it("should trigger popupclose on marker when popup closes", function () {
		var marker1 = new L.Marker(new L.LatLng(55.8, 37.6));
		var marker2 = new L.Marker(new L.LatLng(57.123076977278, 44.861962891635));

		map.addLayer(marker1);
		map.addLayer(marker2);

		marker1.bindPopup('Popup1');
		marker2.bindPopup('Popup2');

		var spy = sinon.spy();

		marker1.on('popupclose', spy);

		expect(spy.called).to.be(false);
		marker2.openPopup();
		expect(spy.called).to.be(false);
		marker1.openPopup();
		expect(spy.called).to.be(false);
		marker2.openPopup();
		expect(spy.called).to.be(true);
		marker1.openPopup();
		marker1.closePopup();
		expect(spy.callCount).to.be(2);
	});
});


describe("Map", function () {
	var map,
		spy;
	beforeEach(function () {
		map = L.map(document.createElement('div'));
	});

	describe("#remove", function () {
		it("fires an unload event if loaded", function () {
			var container = document.createElement('div'),
			    map = new L.Map(container).setView([0, 0], 0),
				spy = sinon.spy();
			map.on('unload', spy);
			map.remove();
			expect(spy.called).to.be.ok();
		});

		it("fires no unload event if not loaded", function () {
			var container = document.createElement('div'),
			    map = new L.Map(container),
				spy = sinon.spy();
			map.on('unload', spy);
			map.remove();
			expect(spy.called).not.to.be.ok();
		});

		describe("corner case checking", function () {
			it("throws an exception upon reinitialization", function () {
				var container = document.createElement('div'),
					map = new L.Map(container);
				expect(function () {
					L.map(container);
				}).to.throwException(function (e) {
					expect(e.message).to.eql("Map container is already initialized.");
				});
				map.remove();
			});

			it("throws an exception if a container is not found", function () {
				expect(function () {
					L.map('nonexistentdivelement');
				}).to.throwException(function (e) {
					expect(e.message).to.eql("Map container not found.");
				});
				map.remove();
			});
		});

		it("undefines container._leaflet", function () {
			var container = document.createElement('div'),
			    map = new L.Map(container);
			map.remove();
			expect(container._leaflet).to.be(undefined);
		});

		it("unbinds events", function () {
			var container = document.createElement('div'),
			    map = new L.Map(container).setView([0, 0], 1),
				spy = sinon.spy();

			map.on('click dblclick mousedown mouseup mousemove', spy);
			map.remove();

			happen.click(container);
			happen.dblclick(container);
			happen.mousedown(container);
			happen.mouseup(container);
			happen.mousemove(container);

			expect(spy.called).to.not.be.ok();
		});
	});

	describe('#getCenter', function () {
		it('throws if not set before', function () {
			expect(function () {
				map.getCenter();
			}).to.throwError();
		});

		it('returns a precise center when zoomed in after being set (#426)', function () {
			var center = L.latLng(10, 10);
			map.setView(center, 1);
			map.setZoom(19);
			expect(map.getCenter()).to.eql(center);
		});

		it('returns correct center after invalidateSize (#1919)', function () {
			map.setView(L.latLng(10, 10), 1);
			map.invalidateSize();
			expect(map.getCenter()).not.to.eql(L.latLng(10, 10));
		});
	});

	describe("#whenReady", function () {
		describe("when the map has not yet been loaded", function () {
			it("calls the callback when the map is loaded", function () {
				var spy = sinon.spy();
				map.whenReady(spy);
				expect(spy.called).to.not.be.ok();

				map.setView([0, 0], 1);
				expect(spy.called).to.be.ok();
			});
		});

		describe("when the map has already been loaded", function () {
			it("calls the callback immediately", function () {
				var spy = sinon.spy();
				map.setView([0, 0], 1);
				map.whenReady(spy);

				expect(spy.called).to.be.ok();
			});
		});
	});

	describe("#setView", function () {
		it("sets the view of the map", function () {
			expect(map.setView([51.505, -0.09], 13)).to.be(map);
			expect(map.getZoom()).to.be(13);
			expect(map.getCenter().distanceTo([51.505, -0.09])).to.be.lessThan(5);
		});
		it("can be passed without a zoom specified", function () {
			map.setZoom(13);
			expect(map.setView([51.605, -0.11])).to.be(map);
			expect(map.getZoom()).to.be(13);
			expect(map.getCenter().distanceTo([51.605, -0.11])).to.be.lessThan(5);
		});
	});

	describe("#getBounds", function () {
		it("is safe to call from within a moveend callback during initial load (#1027)", function () {
			map.on("moveend", function () {
				map.getBounds();
			});

			map.setView([51.505, -0.09], 13);
		});
	});

	describe('#setMaxBounds', function () {
		it("aligns pixel-wise map view center with maxBounds center if it cannot move view bounds inside maxBounds (#1908)", function () {
			var container = map.getContainer();
			// large view, cannot fit within maxBounds
			container.style.width = container.style.height = "1000px";
			document.body.appendChild(container);
			// maxBounds
			var bounds = L.latLngBounds([51.5, -0.05], [51.55, 0.05]);
			map.setMaxBounds(bounds, {animate: false});
			// set view outside
			map.setView(L.latLng([53.0, 0.15]), 12, {animate: false});
			// get center of bounds in pixels
			var boundsCenter = map.project(bounds.getCenter()).round();
			expect(map.project(map.getCenter()).round()).to.eql(boundsCenter);
			document.body.removeChild(container);
		});
		it("moves map view within maxBounds by changing one coordinate", function () {
			var container = map.getContainer();
			// small view, can fit within maxBounds
			container.style.width = container.style.height = "200px";
			document.body.appendChild(container);
			// maxBounds
			var bounds = L.latLngBounds([51, -0.2], [52, 0.2]);
			map.setMaxBounds(bounds, {animate: false});
			// set view outside maxBounds on one direction only
			// leaves untouched the other coordinate (that is not already centered)
			var initCenter = [53.0, 0.1];
			map.setView(L.latLng(initCenter), 16, {animate: false});
			// one pixel coordinate hasn't changed, the other has
			var pixelCenter = map.project(map.getCenter()).round();
			var pixelInit = map.project(initCenter).round();
			expect(pixelCenter.x).to.eql(pixelInit.x);
			expect(pixelCenter.y).not.to.eql(pixelInit.y);
			// the view is inside the bounds
			expect(bounds.contains(map.getBounds())).to.be(true);
			document.body.removeChild(container);
		});
	});

	describe("#getMinZoom and #getMaxZoom", function () {
		describe('#getMinZoom', function () {
			it('returns 0 if not set by Map options or TileLayer options', function () {
				var map = L.map(document.createElement('div'));
				expect(map.getMinZoom()).to.be(0);
			});
		});

		it("minZoom and maxZoom options overrides any minZoom and maxZoom set on layers", function () {

			var map = L.map(document.createElement('div'), {minZoom: 2, maxZoom: 20});

			L.tileLayer("{z}{x}{y}", {minZoom: 4, maxZoom: 10}).addTo(map);
			L.tileLayer("{z}{x}{y}", {minZoom: 6, maxZoom: 17}).addTo(map);
			L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 22}).addTo(map);

			expect(map.getMinZoom()).to.be(2);
			expect(map.getMaxZoom()).to.be(20);
		});
	});

	describe("#hasLayer", function () {
		it("returns false when passed undefined, null, or false", function () {
			var map = L.map(document.createElement('div'));
			expect(map.hasLayer(undefined)).to.equal(false);
			expect(map.hasLayer(null)).to.equal(false);
			expect(map.hasLayer(false)).to.equal(false);
		});
	});

	function layerSpy() {
		var layer = new L.Layer();
		layer.onAdd = sinon.spy();
		layer.onRemove = sinon.spy();
		return layer;
	}

	describe("#addLayer", function () {

		it("calls layer.onAdd immediately if the map is ready", function () {
			var layer = layerSpy();
			map.setView([0, 0], 0);
			map.addLayer(layer);
			expect(layer.onAdd.called).to.be.ok();
		});

		it("calls layer.onAdd when the map becomes ready", function () {
			var layer = layerSpy();
			map.addLayer(layer);
			expect(layer.onAdd.called).not.to.be.ok();
			map.setView([0, 0], 0);
			expect(layer.onAdd.called).to.be.ok();
		});

		it("does not call layer.onAdd if the layer is removed before the map becomes ready", function () {
			var layer = layerSpy();
			map.addLayer(layer);
			map.removeLayer(layer);
			map.setView([0, 0], 0);
			expect(layer.onAdd.called).not.to.be.ok();
		});

		it("fires a layeradd event immediately if the map is ready", function () {
			var layer = layerSpy(),
			    spy = sinon.spy();
			map.on('layeradd', spy);
			map.setView([0, 0], 0);
			map.addLayer(layer);
			expect(spy.called).to.be.ok();
		});

		it("fires a layeradd event when the map becomes ready", function () {
			var layer = layerSpy(),
			    spy = sinon.spy();
			map.on('layeradd', spy);
			map.addLayer(layer);
			expect(spy.called).not.to.be.ok();
			map.setView([0, 0], 0);
			expect(spy.called).to.be.ok();
		});

		it("does not fire a layeradd event if the layer is removed before the map becomes ready", function () {
			var layer = layerSpy(),
			    spy = sinon.spy();
			map.on('layeradd', spy);
			map.addLayer(layer);
			map.removeLayer(layer);
			map.setView([0, 0], 0);
			expect(spy.called).not.to.be.ok();
		});

		it("adds the layer before firing layeradd", function (done) {
			var layer = layerSpy();
			map.on('layeradd', function () {
				expect(map.hasLayer(layer)).to.be.ok();
				done();
			});
			map.setView([0, 0], 0);
			map.addLayer(layer);
		});

		describe("When the first layer is added to a map", function () {
			it("fires a zoomlevelschange event", function () {
				var spy = sinon.spy();
				map.on("zoomlevelschange", spy);
				expect(spy.called).not.to.be.ok();
				L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map);
				expect(spy.called).to.be.ok();
			});
		});

		describe("when a new layer with greater zoomlevel coverage than the current layer is added to a map", function () {
			it("fires a zoomlevelschange event", function () {
				var spy = sinon.spy();
				L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map);
				map.on("zoomlevelschange", spy);
				expect(spy.called).not.to.be.ok();
				L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 15}).addTo(map);
				expect(spy.called).to.be.ok();
			});
		});

		describe("when a new layer with the same or lower zoomlevel coverage as the current layer is added to a map", function () {
			it("fires no zoomlevelschange event", function () {
				var spy = sinon.spy();
				L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map);
				map.on("zoomlevelschange", spy);
				expect(spy.called).not.to.be.ok();
				L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map);
				expect(spy.called).not.to.be.ok();
				L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 5}).addTo(map);
				expect(spy.called).not.to.be.ok();
			});
		});
	});

	describe("#removeLayer", function () {
		it("calls layer.onRemove if the map is ready", function () {
			var layer = layerSpy();
			map.setView([0, 0], 0);
			map.addLayer(layer);
			map.removeLayer(layer);
			expect(layer.onRemove.called).to.be.ok();
		});

		it("does not call layer.onRemove if the layer was not added", function () {
			var layer = layerSpy();
			map.setView([0, 0], 0);
			map.removeLayer(layer);
			expect(layer.onRemove.called).not.to.be.ok();
		});

		it("does not call layer.onRemove if the map is not ready", function () {
			var layer = layerSpy();
			map.addLayer(layer);
			map.removeLayer(layer);
			expect(layer.onRemove.called).not.to.be.ok();
		});

		it("fires a layerremove event if the map is ready", function () {
			var layer = layerSpy(),
			    spy = sinon.spy();
			map.on('layerremove', spy);
			map.setView([0, 0], 0);
			map.addLayer(layer);
			map.removeLayer(layer);
			expect(spy.called).to.be.ok();
		});

		it("does not fire a layerremove if the layer was not added", function () {
			var layer = layerSpy(),
			    spy = sinon.spy();
			map.on('layerremove', spy);
			map.setView([0, 0], 0);
			map.removeLayer(layer);
			expect(spy.called).not.to.be.ok();
		});

		it("does not fire a layerremove if the map is not ready", function () {
			var layer = layerSpy(),
			    spy = sinon.spy();
			map.on('layerremove', spy);
			map.addLayer(layer);
			map.removeLayer(layer);
			expect(spy.called).not.to.be.ok();
		});

		it("removes the layer before firing layerremove", function (done) {
			var layer = layerSpy();
			map.on('layerremove', function () {
				expect(map.hasLayer(layer)).not.to.be.ok();
				done();
			});
			map.setView([0, 0], 0);
			map.addLayer(layer);
			map.removeLayer(layer);
		});

		describe("when the last tile layer on a map is removed", function () {
			it("fires a zoomlevelschange event", function () {
				map.whenReady(function () {
					var spy = sinon.spy();
					var tl = L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map);

					map.on("zoomlevelschange", spy);
					expect(spy.called).not.to.be.ok();
					map.removeLayer(tl);
					expect(spy.called).to.be.ok();
				});
			});
		});

		describe("when a tile layer is removed from a map and it had greater zoom level coverage than the remainding layer", function () {
			it("fires a zoomlevelschange event", function () {
				map.whenReady(function () {
					var spy = sinon.spy(),
						tl = L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map),
					    t2 = L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 15}).addTo(map);

					map.on("zoomlevelschange", spy);
					expect(spy.called).to.not.be.ok();
					map.removeLayer(t2);
					expect(spy.called).to.be.ok();
				});
			});
		});

		describe("when a tile layer is removed from a map it and it had lesser or the sa,e zoom level coverage as the remainding layer(s)", function () {
			it("fires no zoomlevelschange event", function () {
				map.whenReady(function () {
					var tl = L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map),
					    t2 = L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 10}).addTo(map),
					    t3 = L.tileLayer("{z}{x}{y}", {minZoom: 0, maxZoom: 5}).addTo(map);

					map.on("zoomlevelschange", spy);
					expect(spy).not.toHaveBeenCalled();
					map.removeLayer(t2);
					expect(spy).not.toHaveBeenCalled();
					map.removeLayer(t3);
					expect(spy).not.toHaveBeenCalled();
				});
			});
		});
	});

	describe("#eachLayer", function () {
		it("returns self", function () {
			expect(map.eachLayer(function () {})).to.be(map);
		});

		it("calls the provided function for each layer", function () {
			var t1 = L.tileLayer("{z}{x}{y}").addTo(map),
			    t2 = L.tileLayer("{z}{x}{y}").addTo(map),
				spy = sinon.spy();

			map.eachLayer(spy);

			expect(spy.callCount).to.eql(2);
			expect(spy.firstCall.args).to.eql([t1]);
			expect(spy.secondCall.args).to.eql([t2]);
		});

		it("calls the provided function with the provided context", function () {
			var t1 = L.tileLayer("{z}{x}{y}").addTo(map),
				spy = sinon.spy();

			map.eachLayer(spy, map);

			expect(spy.thisValues[0]).to.eql(map);
		});
	});

	describe("#invalidateSize", function () {
		var container,
		    origWidth = 100,
			clock;

		beforeEach(function () {
			container = map.getContainer();
			container.style.width = origWidth + "px";
			document.body.appendChild(container);
			map.setView([0, 0], 0);
			map.invalidateSize({pan: false});
			clock = sinon.useFakeTimers();
		});

		afterEach(function () {
			document.body.removeChild(container);
			clock.restore();
		});

		it("pans by the right amount when growing in 1px increments", function () {
			container.style.width = (origWidth + 1) + "px";
			map.invalidateSize();
			expect(map._getMapPanePos().x).to.be(1);

			container.style.width = (origWidth + 2) + "px";
			map.invalidateSize();
			expect(map._getMapPanePos().x).to.be(1);

			container.style.width = (origWidth + 3) + "px";
			map.invalidateSize();
			expect(map._getMapPanePos().x).to.be(2);
		});

		it("pans by the right amount when shrinking in 1px increments", function () {
			container.style.width = (origWidth - 1) + "px";
			map.invalidateSize();
			expect(map._getMapPanePos().x).to.be(0);

			container.style.width = (origWidth - 2) + "px";
			map.invalidateSize();
			expect(map._getMapPanePos().x).to.be(-1);

			container.style.width = (origWidth - 3) + "px";
			map.invalidateSize();
			expect(map._getMapPanePos().x).to.be(-1);
		});

		it("pans back to the original position after growing by an odd size and back", function () {
			container.style.width = (origWidth + 5) + "px";
			map.invalidateSize();

			container.style.width = origWidth + "px";
			map.invalidateSize();

			expect(map._getMapPanePos().x).to.be(0);
		});

		it("emits no move event if the size has not changed", function () {
			var spy = sinon.spy();
			map.on("move", spy);

			map.invalidateSize();

			expect(spy.called).not.to.be.ok();
		});

		it("emits a move event if the size has changed", function () {
			var spy = sinon.spy();
			map.on("move", spy);

			container.style.width = (origWidth + 5) + "px";
			map.invalidateSize();

			expect(spy.called).to.be.ok();
		});

		it("emits a moveend event if the size has changed", function () {
			var spy = sinon.spy();
			map.on("moveend", spy);

			container.style.width = (origWidth + 5) + "px";
			map.invalidateSize();

			expect(spy.called).to.be.ok();
		});

		it("debounces the moveend event if the debounceMoveend option is given", function () {
			var spy = sinon.spy();
			map.on("moveend", spy);

			container.style.width = (origWidth + 5) + "px";
			map.invalidateSize({debounceMoveend: true});

			expect(spy.called).not.to.be.ok();

			clock.tick(200);

			expect(spy.called).to.be.ok();
		});
	});
});


/*
 * L.Control.Zoom is used for the default zoom buttons on the map.
 */

L.Control.Zoom = L.Control.extend({
	options: {
		position: 'topleft',
		zoomInText: '+',
		zoomInTitle: 'Zoom in',
		zoomOutText: '-',
		zoomOutTitle: 'Zoom out'
	},

	onAdd: function (map) {
		var zoomName = 'leaflet-control-zoom',
		    container = L.DomUtil.create('div', zoomName + ' leaflet-bar'),
		    options = this.options;

		this._zoomInButton  = this._createButton(options.zoomInText, options.zoomInTitle,
		        zoomName + '-in',  container, this._zoomIn);
		this._zoomOutButton = this._createButton(options.zoomOutText, options.zoomOutTitle,
		        zoomName + '-out', container, this._zoomOut);

		this._updateDisabled();
		map.on('zoomend zoomlevelschange', this._updateDisabled, this);

		return container;
	},

	onRemove: function (map) {
		map.off('zoomend zoomlevelschange', this._updateDisabled, this);
	},

	_zoomIn: function (e) {
		this._map.zoomIn(e.shiftKey ? 3 : 1);
	},

	_zoomOut: function (e) {
		this._map.zoomOut(e.shiftKey ? 3 : 1);
	},

	_createButton: function (html, title, className, container, fn) {
		var link = L.DomUtil.create('a', className, container);
		link.innerHTML = html;
		link.href = '#';
		link.title = title;

		L.DomEvent
		    .on(link, 'mousedown dblclick', L.DomEvent.stopPropagation)
		    .on(link, 'click', L.DomEvent.stop)
		    .on(link, 'click', fn, this)
		    .on(link, 'click', this._refocusOnMap, this);

		return link;
	},

	_updateDisabled: function () {
		var map = this._map,
			className = 'leaflet-disabled';

		L.DomUtil.removeClass(this._zoomInButton, className);
		L.DomUtil.removeClass(this._zoomOutButton, className);

		if (map._zoom === map.getMinZoom()) {
			L.DomUtil.addClass(this._zoomOutButton, className);
		}
		if (map._zoom === map.getMaxZoom()) {
			L.DomUtil.addClass(this._zoomInButton, className);
		}
	}
});

L.Map.mergeOptions({
	zoomControl: true
});

L.Map.addInitHook(function () {
	if (this.options.zoomControl) {
		this.zoomControl = new L.Control.Zoom();
		this.addControl(this.zoomControl);
	}
});

L.control.zoom = function (options) {
	return new L.Control.Zoom(options);
};


// if we just removed the transition property, the element would jump to its final position,

// so we need to make it stay at the current position

L.Util.falseFn(this._el.offsetWidth); // force reflow in case we are about to start a new animation

keep keep keep keep keep replace keep keep keep keep


/*
 * L.Marker is used to display clickable/draggable icons on the map.
 */

L.Marker = L.Layer.extend({

	options: {
		pane: 'markerPane',

		icon: new L.Icon.Default(),
		// title: '',
		// alt: '',
		clickable: true,
		// draggable: false,
		keyboard: true,
		zIndexOffset: 0,
		opacity: 1,
		// riseOnHover: false,
		riseOffset: 250
	},

	initialize: function (latlng, options) {
		L.setOptions(this, options);
		this._latlng = L.latLng(latlng);
	},

	onAdd: function (map) {
		this._zoomAnimated = this._zoomAnimated && map.options.markerZoomAnimation;

		this._initIcon();
		this.update();
	},

	onRemove: function () {
		if (this.dragging) {
			this.dragging.disable();
		}

		this._removeIcon();
		this._removeShadow();
	},

	getEvents: function () {
		var events = {viewreset: this.update};

		if (this._zoomAnimated) {
			events.zoomanim = this._animateZoom;
		}

		return events;
	},

	getLatLng: function () {
		return this._latlng;
	},

	setLatLng: function (latlng) {
		this._latlng = L.latLng(latlng);
		this.update();
		return this.fire('move', {latlng: this._latlng});
	},

	setZIndexOffset: function (offset) {
		this.options.zIndexOffset = offset;
		return this.update();
	},

	setIcon: function (icon) {

		this.options.icon = icon;

		if (this._map) {
			this._initIcon();
			this.update();
		}

		if (this._popup) {
			this.bindPopup(this._popup);
		}

		return this;
	},

	update: function () {

		if (this._icon) {
			var pos = this._map.latLngToLayerPoint(this._latlng).round();
			this._setPos(pos);
		}

		return this;
	},

	_initIcon: function () {
		var options = this.options,
		    classToAdd = 'leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide');

		var icon = options.icon.createIcon(this._icon),
			addIcon = false;

		// if we're not reusing the icon, remove the old one and init new one
		if (icon !== this._icon) {
			if (this._icon) {
				this._removeIcon();
			}
			addIcon = true;

			if (options.title) {
				icon.title = options.title;
			}
			if (options.alt) {
				icon.alt = options.alt;
			}
		}

		L.DomUtil.addClass(icon, classToAdd);

		if (options.keyboard) {
			icon.tabIndex = '0';
		}

		this._icon = icon;
		this._initInteraction();

		if (options.riseOnHover) {
			L.DomEvent.on(icon, {
				mouseover: this._bringToFront,
				mouseout: this._resetZIndex
			}, this);
		}

		var newShadow = options.icon.createShadow(this._shadow),
			addShadow = false;

		if (newShadow !== this._shadow) {
			this._removeShadow();
			addShadow = true;
		}

		if (newShadow) {
			L.DomUtil.addClass(newShadow, classToAdd);
		}
		this._shadow = newShadow;

		if (options.opacity < 1) {
			this._updateOpacity();
		}

		if (addIcon) {
			this.getPane().appendChild(this._icon);
		}
		if (newShadow && addShadow) {
			this.getPane('shadowPane').appendChild(this._shadow);
		}
	},

	_removeIcon: function () {
		if (this.options.riseOnHover) {
			L.DomEvent.off(this._icon, {
				mouseover: this._bringToFront,
			    mouseout: this._resetZIndex
			}, this);
		}

		L.DomUtil.remove(this._icon);

		this._icon = null;
	},

	_removeShadow: function () {
		if (this._shadow) {
			L.DomUtil.remove(this._shadow);
		}
		this._shadow = null;
	},

	_setPos: function (pos) {
		L.DomUtil.setPosition(this._icon, pos);

		if (this._shadow) {
			L.DomUtil.setPosition(this._shadow, pos);
		}

		this._zIndex = pos.y + this.options.zIndexOffset;

		this._resetZIndex();
	},

	_updateZIndex: function (offset) {
		this._icon.style.zIndex = this._zIndex + offset;
	},

	_animateZoom: function (opt) {
		var pos = this._map._latLngToNewLayerPoint(this._latlng, opt.zoom, opt.center).round();

		this._setPos(pos);
	},

	_initInteraction: function () {

		if (!this.options.clickable) { return; }

		L.DomUtil.addClass(this._icon, 'leaflet-clickable');

		L.DomEvent.on(this._icon, 'click dblclick mousedown mouseup mouseover mouseout contextmenu keypress',
				this._fireMouseEvent, this);

		if (L.Handler.MarkerDrag) {
			this.dragging = new L.Handler.MarkerDrag(this);

			if (this.options.draggable) {
				this.dragging.enable();
			}
		}
	},

	_fireMouseEvent: function (e, type) {
		// to prevent outline when clicking on keyboard-focusable marker
		if (e.type === 'mousedown') {
			L.DomEvent.preventDefault(e);
		}

		if (e.type === 'click' && this.dragging && this.dragging.moved()) { return; }

		if (e.type === 'keypress' && e.keyCode === 13) {
			type = 'click';
		}

		this._map._fireMouseEvent(this, e, type, true, this._latlng);
	},

	setOpacity: function (opacity) {
		this.options.opacity = opacity;
		if (this._map) {
			this._updateOpacity();
		}

		return this;
	},

	_updateOpacity: function () {
		var opacity = this.options.opacity;

		L.DomUtil.setOpacity(this._icon, opacity);

		if (this._shadow) {
			L.DomUtil.setOpacity(this._shadow, opacity);
		}
	},

	_bringToFront: function () {
		this._updateZIndex(this.options.riseOffset);
	},

	_resetZIndex: function () {
		this._updateZIndex(0);
	}
});

L.marker = function (latlng, options) {
	return new L.Marker(latlng, options);
};


describe('DomUtil', function () {
	var el;

	beforeEach(function () {
		el = document.createElement('div');
		el.style.position = 'absolute';
		el.style.top = el.style.left = '-10000px';
		document.body.appendChild(el);
	});

	afterEach(function () {
		document.body.removeChild(el);
	});

	describe('#get', function () {
		it('gets element by id if the given argument is string', function () {
			el.id = 'testId';
			expect(L.DomUtil.get(el.id)).to.eql(el);
		});

		it('returns the element if it is given as an argument', function () {
			expect(L.DomUtil.get(el)).to.eql(el);
		});
	});

	describe('#addClass, #removeClass, #hasClass', function () {
		it('has defined class for test element', function () {
			el.className = 'bar foo baz ';
			expect(L.DomUtil.hasClass(el, 'foo')).to.be.ok();
			expect(L.DomUtil.hasClass(el, 'bar')).to.be.ok();
			expect(L.DomUtil.hasClass(el, 'baz')).to.be.ok();
			expect(L.DomUtil.hasClass(el, 'boo')).to.not.be.ok();
		});

		it('adds or removes the class', function () {
			el.className = '';
			L.DomUtil.addClass(el, 'foo');

			expect(el.className).to.eql('foo');
			expect(L.DomUtil.hasClass(el, 'foo')).to.be.ok();

			L.DomUtil.addClass(el, 'bar');
			expect(el.className).to.eql('foo bar');
			expect(L.DomUtil.hasClass(el, 'foo')).to.be.ok();

			L.DomUtil.removeClass(el, 'foo');
			expect(el.className).to.eql('bar');
			expect(L.DomUtil.hasClass(el, 'foo')).to.not.be.ok();

			el.className = 'foo bar barz';
			L.DomUtil.removeClass(el, 'bar');
			expect(el.className).to.eql('foo barz');
		});
	});

	// describe('#setPosition', noSpecs);

	// describe('#getStyle', noSpecs);
});


/*
 * L.Map.Keyboard is handling keyboard interaction with the map, enabled by default.
 */

L.Map.mergeOptions({
	keyboard: true,
	keyboardPanOffset: 80,
	keyboardZoomOffset: 1
});

L.Map.Keyboard = L.Handler.extend({

	keyCodes: {
		left:    [37],
		right:   [39],
		down:    [40],
		up:      [38],
		zoomIn:  [187, 107, 61, 171],
		zoomOut: [189, 109, 173]
	},

	initialize: function (map) {
		this._map = map;

		this._setPanOffset(map.options.keyboardPanOffset);
		this._setZoomOffset(map.options.keyboardZoomOffset);
	},

	addHooks: function () {
		var container = this._map._container;

		// make the container focusable by tabbing
		if (container.tabIndex === -1) {
			container.tabIndex = '0';
		}

		L.DomEvent.on(container, {
		    focus: this._onFocus,
		    blur: this._onBlur,
		    mousedown: this._onMouseDown
		}, this);

		this._map.on({
			focus: this._addHooks,
		    blur: this._removeHooks
		}, this);
	},

	removeHooks: function () {
		this._removeHooks();

		L.DomEvent.off(this._map._container, {
		    focus: this._onFocus,
		    blur: this._onBlur,
		    mousedown: this._onMouseDown
		}, this);

		this._map.off({
			focus: this._addHooks,
		    blur: this._removeHooks
		}, this);
	},

	_onMouseDown: function () {
		if (this._focused) { return; }

		var body = document.body,
		    docEl = document.documentElement,
		    top = body.scrollTop || docEl.scrollTop,
		    left = body.scrollLeft || docEl.scrollLeft;

		this._map._container.focus();

		window.scrollTo(left, top);
	},

	_onFocus: function () {
		this._focused = true;
		this._map.fire('focus');
	},

	_onBlur: function () {
		this._focused = false;
		this._map.fire('blur');
	},

	_setPanOffset: function (pan) {
		var keys = this._panKeys = {},
		    codes = this.keyCodes,
		    i, len;

		for (i = 0, len = codes.left.length; i < len; i++) {
			keys[codes.left[i]] = [-1 * pan, 0];
		}
		for (i = 0, len = codes.right.length; i < len; i++) {
			keys[codes.right[i]] = [pan, 0];
		}
		for (i = 0, len = codes.down.length; i < len; i++) {
			keys[codes.down[i]] = [0, pan];
		}
		for (i = 0, len = codes.up.length; i < len; i++) {
			keys[codes.up[i]] = [0, -1 * pan];
		}
	},

	_setZoomOffset: function (zoom) {
		var keys = this._zoomKeys = {},
		    codes = this.keyCodes,
		    i, len;

		for (i = 0, len = codes.zoomIn.length; i < len; i++) {
			keys[codes.zoomIn[i]] = zoom;
		}
		for (i = 0, len = codes.zoomOut.length; i < len; i++) {
			keys[codes.zoomOut[i]] = -zoom;
		}
	},

	_addHooks: function () {
		L.DomEvent.on(document, 'keydown', this._onKeyDown, this);
	},

	_removeHooks: function () {
		L.DomEvent.off(document, 'keydown', this._onKeyDown, this);
	},

	_onKeyDown: function (e) {
		var key = e.keyCode,
		    map = this._map;

		if (key in this._panKeys) {

			if (map._panAnim && map._panAnim._inProgress) { return; }

			map.panBy(this._panKeys[key]);

			if (map.options.maxBounds) {
				map.panInsideBounds(map.options.maxBounds);
			}

		} else if (key in this._zoomKeys) {
			map.setZoom(map.getZoom() + this._zoomKeys[key]);

		} else {
			return;
		}

		L.DomEvent.stop(e);
	}
});

L.Map.addInitHook('addHandler', 'keyboard', L.Map.Keyboard);


this._draggable = new L.Draggable(map._mapPane, map._container);


var fs = require('fs'),
    jshint = require('jshint'),
    UglifyJS = require('uglify-js'),
    zlib = require('zlib'),

    deps = require('./deps.js').deps;

function getFiles(compsBase32) {
	var memo = {},
	    comps;

	if (compsBase32) {
		comps = parseInt(compsBase32, 32).toString(2).split('');
		console.log('Managing dependencies...');
	}

	function addFiles(srcs) {
		for (var j = 0, len = srcs.length; j < len; j++) {
			memo[srcs[j]] = true;
		}
	}

	for (var i in deps) {
		if (comps) {
			if (parseInt(comps.pop(), 2) === 1) {
				console.log(' * ' + i);
				addFiles(deps[i].src);
			} else {
				console.log('   ' + i);
			}
		} else {
			addFiles(deps[i].src);
		}
	}

	console.log('');

	var files = [];

	for (var src in memo) {
		files.push('src/' + src);
	}

	return files;
}

exports.getFiles = getFiles;

function getSizeDelta(newContent, oldContent, fixCRLF) {
	if (!oldContent) {
		return ' (new)';
	}
	if (newContent === oldContent) {
		return ' (unchanged)';
	}
	if (fixCRLF) {
		newContent = newContent.replace(/\r\n?/g, '\n');
		oldContent = oldContent.replace(/\r\n?/g, '\n');
	}
	var delta = newContent.length - oldContent.length;

	return delta === 0 ? '' : ' (' + (delta > 0 ? '+' : '') + delta + ' bytes)';
}

function loadSilently(path) {
	try {
		return fs.readFileSync(path, 'utf8');
	} catch (e) {
		return null;
	}
}

function combineFiles(files) {
	var content = '';
	for (var i = 0, len = files.length; i < len; i++) {
		content += fs.readFileSync(files[i], 'utf8') + '\n\n';
	}
	return content;
}

function bytesToKB(bytes) {
    return (bytes / 1024).toFixed(2) + ' KB';
};

exports.build = function (callback, version, compsBase32, buildName) {

	var files = getFiles(compsBase32);

	console.log('Concatenating and compressing ' + files.length + ' files...');

	var copy = fs.readFileSync('src/copyright.js', 'utf8').replace('{VERSION}', version),
	    intro = '(function (window, document, undefined) {',
	    outro = '}(window, document));',
	    newSrc = copy + intro + combineFiles(files) + outro,

	    pathPart = 'dist/leaflet' + (buildName ? '-' + buildName : ''),
	    srcPath = pathPart + '-src.js',

	    oldSrc = loadSilently(srcPath),
	    srcDelta = getSizeDelta(newSrc, oldSrc, true);

	console.log('\tUncompressed: ' + bytesToKB(newSrc.length) + srcDelta);

	if (newSrc !== oldSrc) {
		fs.writeFileSync(srcPath, newSrc);
		console.log('\tSaved to ' + srcPath);
	}

	var path = pathPart + '.js',
	    oldCompressed = loadSilently(path),
	    newCompressed = copy + UglifyJS.minify(newSrc, {
	        warnings: true,
	        fromString: true
	    }).code,
	    delta = getSizeDelta(newCompressed, oldCompressed);

	console.log('\tCompressed: ' + bytesToKB(newCompressed.length) + delta);

	var newGzipped,
	    gzippedDelta = '';

	function done() {
		if (newCompressed !== oldCompressed) {
			fs.writeFileSync(path, newCompressed);
			console.log('\tSaved to ' + path);
		}
		console.log('\tGzipped: ' + bytesToKB(newGzipped.length) + gzippedDelta);
		callback();
	}

	zlib.gzip(newCompressed, function (err, gzipped) {
		if (err) { return; }
		newGzipped = gzipped;
		if (oldCompressed && (oldCompressed !== newCompressed)) {
			zlib.gzip(oldCompressed, function (err, oldGzipped) {
				if (err) { return; }
				gzippedDelta = getSizeDelta(gzipped, oldGzipped);
				done();
			});
		} else {
			done();
		}
	});
};

exports.test = function(complete, fail) {
	var karma = require('karma'),
	    testConfig = {configFile : __dirname + '/../spec/karma.conf.js'};

	testConfig.browsers = ['PhantomJS'];

	function isArgv(optName) {
		return process.argv.indexOf(optName) !== -1;
	}

	if (isArgv('--chrome')) {
		testConfig.browsers.push('Chrome');
	}
	if (isArgv('--safari')) {
		testConfig.browsers.push('Safari');
	}
	if (isArgv('--ff')) {
		testConfig.browsers.push('Firefox');
	}
	if (isArgv('--ie')) {
		testConfig.browsers.push('IE');
	}

	if (isArgv('--cov')) {
		testConfig.preprocessors = {
			'src/**/*.js': 'coverage'
		};
		testConfig.coverageReporter = {
			type : 'html',
			dir : 'coverage/'
		};
		testConfig.reporters = ['coverage'];
	}

	console.log('Running tests...');

	karma.server.start(testConfig, function(exitCode) {
		if (!exitCode) {
			console.log('\tTests ran successfully.\n');
			complete();
		} else {
			process.exit(exitCode);
		}
	});
};


<!DOCTYPE html>

	<title>Leaflet debug page</title>

	<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />

	<link rel="stylesheet" href="../../dist/leaflet.css" />

	<link rel="stylesheet" href="../css/mobile.css" />


#map {
	width: 800px;
	height: 600px;
	border: 1px solid #ccc;
	}


return this._draggable && this._draggable._moved;

_onDown: function () {
if (this._map._panAnim) {
this._map._panAnim.stop();
}
},


<!DOCTYPE html>

    <title>Leaflet debug page</title>

    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

    <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.4.5/leaflet.css" />
    <link rel="stylesheet" href="../css/screen.css" />


/*
 * L.FeatureGroup extends L.LayerGroup by introducing mouse events and additional methods
 * shared between a group of interactive layers (like vectors or markers).
 */

L.FeatureGroup = L.LayerGroup.extend({

	addLayer: function (layer) {
		if (this.hasLayer(layer)) {
			return this;
		}

		layer.addEventParent(this);

		L.LayerGroup.prototype.addLayer.call(this, layer);

		if (this._popupContent && layer.bindPopup) {
			layer.bindPopup(this._popupContent, this._popupOptions);
		}

		return this.fire('layeradd', {layer: layer});
	},

	removeLayer: function (layer) {
		if (!this.hasLayer(layer)) {
			return this;
		}
		if (layer in this._layers) {
			layer = this._layers[layer];
		}

		layer.removeEventParent(this);

		L.LayerGroup.prototype.removeLayer.call(this, layer);

		if (this._popupContent) {
			this.invoke('unbindPopup');
		}

		return this.fire('layerremove', {layer: layer});
	},

	bindPopup: function (content, options) {
		this._popupContent = content;
		this._popupOptions = options;
		return this.invoke('bindPopup', content, options);
	},

	openPopup: function (latlng) {
		// open popup on the first layer
		for (var id in this._layers) {
			this._layers[id].openPopup(latlng);
			break;
		}
		return this;
	},

	setStyle: function (style) {
		return this.invoke('setStyle', style);
	},

	bringToFront: function () {
		return this.invoke('bringToFront');
	},

	bringToBack: function () {
		return this.invoke('bringToBack');
	},

	getBounds: function () {
		var bounds = new L.LatLngBounds();

		this.eachLayer(function (layer) {
			bounds.extend(layer.getBounds ? layer.getBounds() : layer.getLatLng());
		});

		return bounds;
	}
});

L.featureGroup = function (layers) {
	return new L.FeatureGroup(layers);
};


<!DOCTYPE html>

	<title>Leaflet debug page</title>

	<link rel="stylesheet" href="../../dist/leaflet.css" />

	<meta name="viewport" content="width=device-width, initial-scale=1.0">

	<link rel="stylesheet" href="../css/screen.css" />

These should all render identically.

		CASE 1: no opacity set on any layers

		CASE 2: opacity set to .99 on overlays but not on basemap

		CASE 3: opacity set on overlays but not on basemap, zIndex option set to 0 on basemap

		CASE 4: opacity set to .99 on overlays but set to 1 on basemap

		CASE 5: opacity set to .99 on all layers

		CASE 6: opacity set to .99 on 1st and 3rd layers and 1 on middle layer


_onDragStart: function () {
var map = this._map;
if (map._panAnim) {
map._panAnim.stop();
}
map
.fire('movestart')
.fire('dragstart');

keep keep keep replace replace replace replace keep keep keep

/*
 * L.PosAnimation is used by Leaflet internally for pan animations.
 */

L.PosAnimation = L.Evented.extend({

	run: function (el, newPos, duration, easeLinearity) { // (HTMLElement, Point[, Number, Number])
		this.stop();

		this._el = el;
		this._inProgress = true;
		this._newPos = newPos;

		this.fire('start');

		el.style[L.DomUtil.TRANSITION] = 'all ' + (duration || 0.25) +
		        's cubic-bezier(0,0,' + (easeLinearity || 0.5) + ',1)';

		L.DomEvent.on(el, L.DomUtil.TRANSITION_END, this._onTransitionEnd, this);
		L.DomUtil.setPosition(el, newPos);

		// toggle reflow, Chrome flickers for some reason if you don't do this
		L.Util.falseFn(el.offsetWidth);

		// there's no native way to track value updates of transitioned properties, so we imitate this
		this._stepTimer = setInterval(L.bind(this._onStep, this), 50);
	},

	stop: function () {
		if (!this._inProgress) { return; }

		// if we just removed the transition property, the element would jump to its final position,
		// so we need to make it stay at the current position

		L.DomUtil.setPosition(this._el, this._getPos());
		this._onTransitionEnd();
		L.Util.falseFn(this._el.offsetWidth); // force reflow in case we are about to start a new animation
	},

	_onStep: function () {
		var stepPos = this._getPos();
		if (!stepPos) {
			this._onTransitionEnd();
			return;
		}
		// jshint camelcase: false
		// make L.DomUtil.getPosition return intermediate position value during animation
		this._el._leaflet_pos = stepPos;

		this.fire('step');
	},

	// you can't easily get intermediate values of properties animated with CSS3 Transitions,
	// we need to parse computed style (in case of transform it returns matrix string)

	_transformRe: /([-+]?(?:\d*\.)?\d+)\D*, ([-+]?(?:\d*\.)?\d+)\D*\)/,

	_getPos: function () {
		var left, top, matches,
		    el = this._el,
		    style = window.getComputedStyle(el);

		if (L.Browser.any3d) {
			matches = style[L.DomUtil.TRANSFORM].match(this._transformRe);
			if (!matches) { return; }
			left = parseFloat(matches[1]);
			top  = parseFloat(matches[2]);
		} else {
			left = parseFloat(style.left);
			top  = parseFloat(style.top);
		}

		return new L.Point(left, top, true);
	},

	_onTransitionEnd: function () {
		L.DomEvent.off(this._el, L.DomUtil.TRANSITION_END, this._onTransitionEnd, this);

		if (!this._inProgress) { return; }
		this._inProgress = false;

		this._el.style[L.DomUtil.TRANSITION] = '';

		// jshint camelcase: false
		// make sure L.DomUtil.getPosition returns the final position value after animation
		this._el._leaflet_pos = this._newPos;

		clearInterval(this._stepTimer);

		this.fire('step').fire('end');
	}

});


{
	"browser": true,
	"node": true,
	"predef": ["define", "L", "expect", "describe", "it", "sinon", "happen", "beforeEach", "afterEach"],
	"strict": false,
	"bitwise": true,
	"camelcase": true,
	"curly": true,
	"eqeqeq": true,
	"forin": false,
	"immed": true,
	"latedef": true,
	"newcap": true,
	"noarg": true,
	"noempty": true,
	"nonew": true,
	"undef": true,
	// "unused": true,
	// "quotmark": "single",
	"indent": 4,
	"trailing": true,
	"white": true,
	"smarttabs": true
	// "maxlen": 120
}


/*
 * L.Handler.ScrollWheelZoom is used by L.Map to enable mouse scroll wheel zoom on the map.
 */

L.Map.mergeOptions({
	scrollWheelZoom: true
});

L.Map.ScrollWheelZoom = L.Handler.extend({
	addHooks: function () {
		L.DomEvent.on(this._map._container, {
			mousewheel: this._onWheelScroll,
			MozMousePixelScroll: L.DomEvent.preventDefault
		}, this);

		this._delta = 0;
	},

	removeHooks: function () {
		L.DomEvent.off(this._map._container, {
			mousewheel: this._onWheelScroll,
			MozMousePixelScroll: L.DomEvent.preventDefault
		}, this);
	},

	_onWheelScroll: function (e) {
		var delta = L.DomEvent.getWheelDelta(e);

		this._delta += delta;
		this._lastMousePos = this._map.mouseEventToContainerPoint(e);

		if (!this._startTime) {
			this._startTime = +new Date();
		}

		var left = Math.max(40 - (+new Date() - this._startTime), 0);

		clearTimeout(this._timer);
		this._timer = setTimeout(L.bind(this._performZoom, this), left);

		L.DomEvent.stop(e);
	},

	_performZoom: function () {
		var map = this._map,
		    delta = this._delta,
		    zoom = map.getZoom();

		delta = delta > 0 ? Math.ceil(delta) : Math.floor(delta);
		delta = Math.max(Math.min(delta, 4), -4);
		delta = map._limitZoom(zoom + delta) - zoom;

		this._delta = 0;
		this._startTime = null;

		if (!delta) { return; }

		if (map.options.scrollWheelZoom === 'center') {
			map.setZoom(zoom + delta);
		} else {
			map.setZoomAround(this._lastMousePos, zoom + delta);
		}
	}
});

L.Map.addInitHook('addHandler', 'scrollWheelZoom', L.Map.ScrollWheelZoom);


/*
 * L.DomUtil contains various utility functions for working with DOM.
 */

L.DomUtil = {
	get: function (id) {
		return typeof id === 'string' ? document.getElementById(id) : id;
	},

	getStyle: function (el, style) {

		var value = el.style[style] || (el.currentStyle && el.currentStyle[style]);

		if ((!value || value === 'auto') && document.defaultView) {
			var css = document.defaultView.getComputedStyle(el, null);
			value = css ? css[style] : null;
		}

		return value === 'auto' ? null : value;
	},

	create: function (tagName, className, container) {

		var el = document.createElement(tagName);
		el.className = className;

		if (container) {
			container.appendChild(el);
		}

		return el;
	},

	remove: function (el) {
		var parent = el.parentNode;
		if (parent) {
			parent.removeChild(el);
		}
	},

	toFront: function (el) {
		el.parentNode.appendChild(el);
	},

	toBack: function (el) {
		var parent = el.parentNode;
		parent.insertBefore(el, parent.firstChild);
	},

	hasClass: function (el, name) {
		if (el.classList !== undefined) {
			return el.classList.contains(name);
		}
		var className = L.DomUtil.getClass(el);
		return className.length > 0 && new RegExp('(^|\\s)' + name + '(\\s|$)').test(className);
	},

	addClass: function (el, name) {
		if (el.classList !== undefined) {
			var classes = L.Util.splitWords(name);
			for (var i = 0, len = classes.length; i < len; i++) {
				el.classList.add(classes[i]);
			}
		} else if (!L.DomUtil.hasClass(el, name)) {
			var className = L.DomUtil.getClass(el);
			L.DomUtil.setClass(el, (className ? className + ' ' : '') + name);
		}
	},

	removeClass: function (el, name) {
		if (el.classList !== undefined) {
			el.classList.remove(name);
		} else {
			L.DomUtil.setClass(el, L.Util.trim((' ' + L.DomUtil.getClass(el) + ' ').replace(' ' + name + ' ', ' ')));
		}
	},

	setClass: function (el, name) {
		if (el.className.baseVal === undefined) {
			el.className = name;
		} else {
			// in case of SVG element
			el.className.baseVal = name;
		}
	},

	getClass: function (el) {
		return el.className.baseVal === undefined ? el.className : el.className.baseVal;
	},

	setOpacity: function (el, value) {

		if ('opacity' in el.style) {
			el.style.opacity = value;

		} else if ('filter' in el.style) {

			var filter = false,
			    filterName = 'DXImageTransform.Microsoft.Alpha';

			// filters collection throws an error if we try to retrieve a filter that doesn't exist
			try {
				filter = el.filters.item(filterName);
			} catch (e) {
				// don't set opacity to 1 if we haven't already set an opacity,
				// it isn't needed and breaks transparent pngs.
				if (value === 1) { return; }
			}

			value = Math.round(value * 100);

			if (filter) {
				filter.Enabled = (value !== 100);
				filter.Opacity = value;
			} else {
				el.style.filter += ' progid:' + filterName + '(opacity=' + value + ')';
			}
		}
	},

	testProp: function (props) {

		var style = document.documentElement.style;

		for (var i = 0; i < props.length; i++) {
			if (props[i] in style) {
				return props[i];
			}
		}
		return false;
	},

	getTranslateString: function (point) {
		// on WebKit browsers, using translate3d instead of translate makes animation smoother
		// as it ensures HW accel is used. Firefox doesn't care (same speed either way).
		var is3d = L.Browser.webkit3d;

		return 'translate' + (is3d ? '3d(' : '(') +
				point.x + 'px,' +
				point.y + 'px' + (is3d ? ',0)' : ')');
	},

	getScaleString: function (scale, origin) {
		return L.DomUtil.getTranslateString(origin.multiplyBy(1 - scale)) + ' scale(' + scale + ') ';
	},

	setPosition: function (el, point, disable3D) { // (HTMLElement, Point[, Boolean])

		// jshint camelcase: false
		el._leaflet_pos = point;

		if (!disable3D && L.Browser.any3d) {
			el.style[L.DomUtil.TRANSFORM] =  L.DomUtil.getTranslateString(point);
		} else {
			el.style.left = point.x + 'px';
			el.style.top = point.y + 'px';
		}
	},

	getPosition: function (el) {
		// this method is only used for elements previously positioned using setPosition,
		// so it's safe to cache the position for performance

		// jshint camelcase: false
		return el._leaflet_pos;
	}
};

(function () {
	// prefix style property names

	L.DomUtil.TRANSFORM = L.DomUtil.testProp(
			['transform', 'WebkitTransform', 'OTransform', 'MozTransform', 'msTransform']);

	// webkitTransition comes first because some browser versions that drop vendor prefix don't do
	// the same for the transitionend event, in particular the Android 4.1 stock browser

	var transition = L.DomUtil.TRANSITION = L.DomUtil.testProp(
			['webkitTransition', 'transition', 'OTransition', 'MozTransition', 'msTransition']);

	L.DomUtil.TRANSITION_END =
			transition === 'webkitTransition' || transition === 'OTransition' ? transition + 'End' : 'transitionend';

	if ('onselectstart' in document) {
		L.DomUtil.disableTextSelection = function () {
			L.DomEvent.on(window, 'selectstart', L.DomEvent.preventDefault);
		};
		L.DomUtil.enableTextSelection = function () {
			L.DomEvent.off(window, 'selectstart', L.DomEvent.preventDefault);
		};

	} else {
		var userSelectProperty = L.DomUtil.testProp(
			['userSelect', 'WebkitUserSelect', 'OUserSelect', 'MozUserSelect', 'msUserSelect']);

		L.DomUtil.disableTextSelection = function () {
			if (userSelectProperty) {
				var style = document.documentElement.style;
				this._userSelect = style[userSelectProperty];
				style[userSelectProperty] = 'none';
			}
		};
		L.DomUtil.enableTextSelection = function () {
			if (userSelectProperty) {
				document.documentElement.style[userSelectProperty] = this._userSelect;
				delete this._userSelect;
			}
		};
	}

	L.DomUtil.disableImageDrag = function () {
		L.DomEvent.on(window, 'dragstart', L.DomEvent.preventDefault);
	};
	L.DomUtil.enableImageDrag = function () {
		L.DomEvent.off(window, 'dragstart', L.DomEvent.preventDefault);
	};
})();


keep keep keep keep replace replace replace keep keep keep keep keep

preact

Fast 3kb React-compatible Virtual DOM library.

microbundle build --raw -f iife src/cjs.js -o dist/preact.min.js

microbundle build --raw --cwd test-utils

microbundle build --raw --cwd compat --globals 'preact/hooks=preactHooks'

microbundle build --raw --cwd jsx-runtime

node ./config/node-13-exports.js && node ./config/compat-entries.js

microbundle watch --raw --format cjs --cwd hooks

microbundle watch --raw --format cjs --cwd compat --globals 'preact/hooks=preactHooks'

run-p test:mocha test:karma:minify test:ts

tsc -p test/ts/ && mocha --require "@babel/register" test/ts/**/*-test.js

mocha --recursive --require "@babel/register" test/shared test/node

cross-env COVERAGE=true BABEL_NO_MODULES=true karma start karma.conf.js --single-run

cross-env COVERAGE=true MINIFY=true BABEL_NO_MODULES=true karma start karma.conf.js --single-run

cross-env BABEL_NO_MODULES=true karma start karma.conf.js --no-single-run

cross-env COVERAGE=false BABEL_NO_MODULES=true karma start karma.conf.js --grep=hooks/test/browser/**.js --no-single-run

cross-env PERFORMANCE=false COVERAGE=false BABEL_NO_MODULES=true karma start karma.conf.js --grep=test-utils/test/shared/**.js --no-single-run

cross-env PERFORMANCE=true COVERAGE=false BABEL_NO_MODULES=true karma start karma.conf.js --grep=test/benchmarks/**.js --single-run

npm run test:karma:bench -- no-single-run

eslint src test debug compat hooks test-utils

preact
react
ui
user interface
virtual dom
vdom
components
dom diff
front-end
framework

The Preact Authors (https://github.com/preactjs/preact/contributors)


import sade from 'sade';
import { generateSingleConfig } from './config.js';
import { defaultDeoptsOptions, runDeopts } from './deopts.js';
import { defaultBenchOptions, runBenches } from './bench.js';

const prog = sade('./scripts');

// Tests:
// - npm start
prog
	.command('config [bench]')
	.describe('Generate the config for the given benchmark HTML file.')
	.action(generateSingleConfig);

// Tests:
// - many* -n 2 -t 0
// - many* -n 2 -t 0 -f preact-local -f preact-v8
// - many* -n 2 -t 0 -f preact-local -f preact-v8 -b chrome
prog
	.command('bench [globs]')
	.describe(
		'Run the benchmarks matching the given globs. The root for the globs is the "src" directory. Specify "all" to run all benchmarks (default). To get more help on options, see polymer/tachometer help. Result table is printed to stdout and written to a csv and json file in the results directory.'
	)
	.example('bench text*')
	.example('bench *.html')
	.example('bench all')
	.example('bench many* -f preact-local -f preact-master')
	.option(
		'--browser, -b',
		'Which browsers to launch in automatic mode, comma-delimited (chrome, chrome-headless, firefox, firefox-headless, safari, edge, ie)',
		defaultBenchOptions.browser
	)
	// TODO: Consider parsing and adding to configs
	// .option(
	// 	'--window-size',
	// 	'"width,height" in pixels of the browser windows that will be created',
	// 	defaultOptions['window-size']
	// )
	.option(
		'--sample-size, -n',
		'Minimum number of times to run each benchmark',
		defaultBenchOptions['sample-size']
	)
	.option(
		'--horizon, -h',
		'The degrees of difference to try and resolve when auto-sampling ("N%" or "Nms", comma-delimited)',
		defaultBenchOptions.horizon
	)
	.option(
		'--timeout, -t',
		'The maximum number of minutes to spend auto-sampling',
		defaultBenchOptions.timeout
	)
	.option(
		'--framework, -f',
		'Which framework(s) to bench. Specify the flag multiple times to compare specific frameworks. Default is all frameworks',
		defaultBenchOptions.framework
	)
	.action(runBenches);

// Tests:
// - (no args)
// - many*
// - many* -f preact-local -f preact-master
prog
	.command('deopts [benchmark]')
	.describe(
		'Run v8-deopt-viewer against the specified benchmark file (defaults to many_updates.html). If a glob is given, only the first matching file will be run'
	)
	.example('deopts many_updates.html')
	.example('deopts many*')
	.example('deopts many* -f preact-local')
	.example('deopts many* -f preact-local -f preact-master')
	.option(
		'--framework, -f',
		'The framework to run the benchmark with.',
		defaultDeoptsOptions.framework
	)
	.option(
		'--timeout, -t',
		'How long in seconds to keep the browser open while the benchmark runs. Passed to v8-deopt-viewer.',
		defaultDeoptsOptions.timeout
	)
	.option(
		'--open',
		'Open the resulting v8-deopt-viewer result in the browser upon completion',
		defaultDeoptsOptions.open
	)
	.action(runDeopts);

prog.parse(process.argv);


import { unstable_batchedUpdates } from 'preact/compat';

describe('unstable_batchedUpdates', () => {
	it('should call the callback', () => {
		const spy = sinon.spy();
		unstable_batchedUpdates(spy);
		expect(spy).to.be.calledOnce;
	});

	it('should call callback with only one arg', () => {
		const spy = sinon.spy();
		unstable_batchedUpdates(spy, 'foo', 'bar');
		expect(spy).to.be.calledWithExactly('foo');
	});
});


import { createElement, render, Component } from 'preact';
import { setupScratch, teardown } from '../_util/helpers';

/** @jsx createElement */

const runSuite = typeof customElements == 'undefined' ? xdescribe : describe;

runSuite('customised built-in elements', () => {
	let scratch;

	beforeEach(() => {
		scratch = setupScratch();
	});

	afterEach(() => {
		teardown(scratch);
	});

	it('should create built in elements correctly', () => {
		class Foo extends Component {
			render() {
				return <div is="built-in" />;
			}
		}

		const spy = sinon.spy();

		class BuiltIn extends HTMLDivElement {
			connectedCallback() {
				spy();
			}
		}

		customElements.define('built-in', BuiltIn, { extends: 'div' });

		render(<Foo />, scratch);

		expect(spy).to.have.been.calledOnce;
	});
});


"dev": true,
"requires": {
"chokidar": "^3.4.3",
"karma-sourcemap-loader": "^0.3.8"
},
"dependencies": {
"karma-sourcemap-loader": {
"version": "0.3.8",

keep keep replace keep keep keep keep keep


<!DOCTYPE html>

		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>create many rows</title>
		<meta name="description" content="creating 10,000 rows" />