इकाई परीक्षण AngularJS निर्देश टेम्पलेट के साथ


122

मेरे पास एक AngularJS निर्देश है जिसमें एक templateUrlपरिभाषित है। मैं जैस्मीन के साथ इसका परीक्षण करने की कोशिश कर रहा हूं।

मेरी जैस्मीन जावास्क्रिप्ट इस की सिफारिश के अनुसार, निम्नलिखित की तरह दिखता है :

describe('module: my.module', function () {
    beforeEach(module('my.module'));

    describe('my-directive directive', function () {
        var scope, $compile;
        beforeEach(inject(function (_$rootScope_, _$compile_, $injector) {
            scope = _$rootScope_;
            $compile = _$compile_;
            $httpBackend = $injector.get('$httpBackend');
            $httpBackend.whenGET('path/to/template.html').passThrough();
        }));

        describe('test', function () {
            var element;
            beforeEach(function () {
                element = $compile(
                    '<my-directive></my-directive>')(scope);
                angular.element(document.body).append(element);
            });

            afterEach(function () {
                element.remove();
            });

            it('test', function () {
                expect(element.html()).toBe('asdf');
            });

        });
    });
});

जब मैं अपनी जैस्मीन की विशेष त्रुटि में इसे चलाता हूं तो मुझे निम्नलिखित त्रुटि मिलती है:

TypeError: Object #<Object> has no method 'passThrough'

सभी मैं चाहता हूँ कि टेम्पलेट के लिए लोड किया जाना है जैसा कि है - मैं उपयोग नहीं करना चाहता respond। मेरा मानना है कि यह यह करने के लिए संबंधित हो सकता है का उपयोग कर ngMock के बजाय ngMockE2E । यदि यह अपराधी है, तो मैं पूर्व के बजाय बाद का उपयोग कैसे करूं?

अग्रिम में धन्यवाद!


1
मैंने .passThrough();उस तरह से उपयोग नहीं किया है, लेकिन डॉक्स से, क्या आपने कुछ ऐसा करने की कोशिश की है: $httpBackend.expectGET('path/to/template.html'); // do action here $httpBackend.flush();मुझे लगता है कि यह आपके उपयोग को बेहतर बनाता है - आप अनुरोध को पकड़ना नहीं चाहते हैं, अर्थात whenGet(), लेकिन इसके बजाय यह भेजे जाने की जांच करें, और फिर वास्तव में भेज दे?
एलेक्स ओसबोर्न

1
उत्तर के लिए धन्यवाद। मुझे नहीं लगता कि यह expectGETअनुरोध भेजता है ... कम से कम बॉक्स से बाहर। में डॉक्स के साथ अपने उदाहरण /auth.pyएक है $httpBackend.whenसे पहले $httpBackend.expectGETऔर $httpBackend.flushकॉल।
जारेड

2
यह सही है, expectGetबस जाँच कर रहा है कि क्या अनुरोध का प्रयास किया गया था।
एलेक्स ओसबोर्न

1
आह। वैसे मुझे $httpBackendमॉक को वास्तव में निर्देशन में दिए गए URL का उपयोग करने templateUrlऔर इसे प्राप्त करने का तरीका बताने की आवश्यकता है। मैंने सोचा था passThroughकि ऐसा करेंगे। क्या आप ऐसा करने का एक अलग तरीका जानते हैं?
जारेड

2
हम्म, मैंने अभी तक बहुत e2e परीक्षण नहीं किया है, लेकिन डॉक्स की जाँच कर रहा है - क्या आपने इसके बजाय e2e बैकएंड का उपयोग करने की कोशिश की है - मुझे लगता है कि इसीलिए आपको कोई विधि पास नहीं मिली - docs.angularjs.org/api/MMockE2E.$httpBackend
एलेक्स ओसबोर्न

जवाबों:


187

आप सही हैं कि यह ngMock से संबंधित है। NgMock मॉड्यूल स्वचालित रूप से प्रत्येक कोणीय परीक्षण के लिए लोड किया जाता है, और यह सेवा के $httpBackendकिसी भी उपयोग को संभालने के लिए मॉक को इनिशियलाइज़ करता है $http, जिसमें टेम्प्लेट लाना शामिल है। टेम्पलेट सिस्टम के माध्यम से टेम्पलेट को लोड करने की कोशिश करता है $httpऔर यह नकली के लिए "अनपेक्षित अनुरोध" बन जाता है।

आपको टेम्प्लेट को प्री-लोड करने के तरीके की आवश्यकता है $templateCacheताकि वे पहले से ही उपलब्ध हों जब एंगुलर उनके लिए उपयोग किए बिना पूछता है $http

पसंदीदा समाधान: कर्म

यदि आप अपने परीक्षण चलाने के लिए कर्मा का उपयोग कर रहे हैं (और आपको होना चाहिए), तो आप इसे एनजी- html2js प्रीप्रोसेसर के साथ आपके लिए टेम्पलेट लोड करने के लिए कॉन्फ़िगर कर सकते हैं । Ng-html2js आपके द्वारा निर्दिष्ट HTML फ़ाइलों को पढ़ता है और उन्हें एक कोणीय मॉड्यूल में परिवर्तित करता है जो प्री-लोड करता है $templateCache

चरण 1: अपने में प्रीप्रोसेसर को सक्षम और कॉन्फ़िगर करें karma.conf.js

// karma.conf.js

preprocessors: {
    "path/to/templates/**/*.html": ["ng-html2js"]
},

ngHtml2JsPreprocessor: {
    // If your build process changes the path to your templates,
    // use stripPrefix and prependPrefix to adjust it.
    stripPrefix: "source/path/to/templates/.*/",
    prependPrefix: "web/path/to/templates/",

    // the name of the Angular module to create
    moduleName: "my.templates"
},

यदि आप अपने ऐप को मचान बनाने के लिए येओमान का उपयोग कर रहे हैं तो यह कॉन्फिग काम करेगा

plugins: [ 
  'karma-phantomjs-launcher', 
  'karma-jasmine', 
  'karma-ng-html2js-preprocessor' 
], 

preprocessors: { 
  'app/views/*.html': ['ng-html2js'] 
}, 

ngHtml2JsPreprocessor: { 
  stripPrefix: 'app/', 
  moduleName: 'my.templates' 
},

चरण 2: अपने परीक्षणों में मॉड्यूल का उपयोग करें

// my-test.js

beforeEach(module("my.templates"));    // load new module containing templates

एक पूर्ण उदाहरण के लिए, कोणीय परीक्षण गुरु वोज्टा जीना से इस विहित उदाहरण को देखें । इसमें एक संपूर्ण सेटअप शामिल है: कर्म विन्यास, टेम्प्लेट, और परीक्षण।

एक गैर-कर्म समाधान

यदि आप किसी भी कारण से कर्म का उपयोग नहीं करते हैं (मेरे पास विरासत ऐप में एक अनम्य निर्माण प्रक्रिया थी) और बस एक ब्राउज़र में परीक्षण कर रहे हैं, तो मैंने पाया है कि आप $httpBackendवास्तविक के लिए टेम्पलेट लाने के लिए एक कच्चे एक्सएचआर का उपयोग करके एनजीमॉक के अधिग्रहण को प्राप्त कर सकते हैं। और इसमें डालें $templateCache। यह समाधान बहुत कम लचीला है, लेकिन इसे अभी के लिए काम मिलता है।

// my-test.js

// Make template available to unit tests without Karma
//
// Disclaimer: Not using Karma may result in bad karma.
beforeEach(inject(function($templateCache) {
    var directiveTemplate = null;
    var req = new XMLHttpRequest();
    req.onload = function() {
        directiveTemplate = this.responseText;
    };
    // Note that the relative path may be different from your unit test HTML file.
    // Using `false` as the third parameter to open() makes the operation synchronous.
    // Gentle reminder that boolean parameters are not the best API choice.
    req.open("get", "../../partials/directiveTemplate.html", false);
    req.send();
    $templateCache.put("partials/directiveTemplate.html", directiveTemplate);
}));

वास्तव में मुश्किल। कर्म का उपयोग करें । इसे सेट करने में थोड़ा काम लगता है, लेकिन यह आपको कमांड लाइन से एक ही बार में कई ब्राउजर में अपने सारे टेस्ट चलाने देता है। तो आपके पास यह आपके निरंतर एकीकरण प्रणाली के हिस्से के रूप में हो सकता है, और / या आप इसे अपने संपादक से एक शॉर्टकट कुंजी बना सकते हैं। ऑल्ट-टैब-रिफ्रेश-एड-इनफिनिटम से बहुत बेहतर है।


6
यह स्पष्ट हो सकता है, लेकिन अगर अन्य एक ही चीज़ पर अटक जाते हैं और जवाब के लिए यहां देखते हैं: मैं इसे अनुभाग में preprocessorsफ़ाइल पैटर्न (जैसे "path/to/templates/**/*.html") को जोड़े बिना भी काम नहीं कर सकता । fileskarma.conf.js
जोहान

1
तो क्या जारी रखने से पहले प्रतिक्रिया का इंतजार नहीं करने के साथ कोई प्रमुख मुद्दे हैं? क्या यह अनुरोध वापस आने पर मूल्य को अपडेट करेगा (IE 30 सेकंड लेता है)?
जैकी

1
@ जैकी मुझे लगता है कि आप "गैर-कर्म" उदाहरण के बारे में बात कर रहे हैं, जहां मैं falseएक्सएचआर की openकॉल के लिए पैरामीटर का उपयोग कर उसे तुल्यकालिक बनाता हूं । यदि आप ऐसा नहीं करते हैं, तो निष्पादन पूरी तरह से जारी रहेगा और टेम्पलेट को लोड किए बिना, अपने परीक्षणों को निष्पादित करना शुरू कर देगा। उसी समस्या पर आपका अधिकार वापस मिल जाता है: 1) टेम्पलेट के लिए अनुरोध निकल जाता है। 2) परीक्षण निष्पादित करना शुरू कर देता है। 3) परीक्षण एक निर्देश को संकलित करता है, और टेम्पलेट अभी भी लोड नहीं किया गया है। 4) कोणीय अपनी $httpसेवा के माध्यम से टेम्पलेट का अनुरोध करता है , जिसका मजाक उड़ाया जाता है। 5) नकली $httpसेवा शिकायत करती है: "अप्रत्याशित अनुरोध"।
1

1
मैं कर्म के बिना ग्रन्ट-जैस्मीन चलाने में सक्षम था।
फ्लेवरस्केप

5
दूसरी बात: आपको stackoverflow.com/a/19077966/859631 के अनुसार कर्म-एनजी- html2js-preprocessor ( npm install --save-dev karma-ng-html2js-preprocessor) स्थापित करने की आवश्यकता है , और इसे अपने प्लगइन्स अनुभाग में जोड़ें । karma.conf.js
विंसेंट

37

मैंने जो काम किया वह खाका कैश हो रहा था और वहां का दृश्य देख रहा था। मैं ngMock का उपयोग नहीं करने पर नियंत्रण नहीं है, यह पता चला है:

beforeEach(inject(function(_$rootScope_, _$compile_, $templateCache) {
    $scope = _$rootScope_;
    $compile = _$compile_;
    $templateCache.put('path/to/template.html', '<div>Here goes the template</div>');
}));

26
यहाँ इस विधि के साथ मेरी शिकायत है ... अब अगर हम html का एक बड़ा टुकड़ा लेने जा रहे हैं जिसे हम टेम्पलेट कैश में एक स्ट्रिंग के रूप में इंजेक्ट करने जा रहे हैं तो हम क्या करेंगे जब हम सामने के छोर पर HTML बदलते हैं ? HTML को टेस्ट में भी बदलें? IMO जो कि एक अटूट उत्तर है और इसका कारण हम टेम्पलेट-ओवर विकल्प के टेम्पलेट का उपयोग करके गए हैं। भले ही मैं निर्देशन में एक बड़े पैमाने पर स्ट्रिंग के रूप में अपने HTML को नापसंद करता हूं - यह html के दो स्थानों को अपडेट नहीं करने का सबसे स्थायी समाधान है। कौन सा ज्यादा इमेजिंग नहीं लेता है जो समय के साथ html मेल नहीं खा सकता है।
स्टेन मुचो

12

इसे जोड़कर इस प्रारंभिक समस्या को हल किया जा सकता है:

beforeEach(angular.mock.module('ngMockE2E'));

ऐसा इसलिए है क्योंकि यह डिफ़ॉल्ट रूप से ngMock मॉड्यूल में $ httpBackend खोजने की कोशिश करता है और यह पूर्ण नहीं है।


1
वास्तव में यह मूल प्रश्न का सही उत्तर है (यह वही है जिसने मेरी मदद की है)।
Mat

यह कोशिश की, लेकिन passThrough () अभी भी मेरे लिए काम नहीं किया। इसने अभी भी "अनपेक्षित अनुरोध" त्रुटि दी है।
frodo2975

8

समाधान मैं पहुंचा चमेली- jquery.js और एक प्रॉक्सी सर्वर की जरूरत है।

मैंने इन चरणों का पालन किया:

  1. कर्म में:

अपनी फ़ाइलों में चमेली- jquery.js जोड़ें

files = [
    JASMINE,
    JASMINE_ADAPTER,
    ...,
    jasmine-jquery-1.3.1,
    ...
]

एक प्रॉक्सी सर्वर जोड़ें जो आपके फिक्स्चर को सर्वर करेगा

proxies = {
    '/' : 'http://localhost:3502/'
};
  1. आपकी कल्पना में

    वर्णन ('MySpec', function () {var $ गुंजाइश, टेम्प्लेट; jasmine.getFixtures ()। जुड़नारपाठ = 'सार्वजनिक / विभाजन /'; // कस्टम पथ ताकि आप वास्तविक टेम्पलेट की सेवा कर सकें जिसका उपयोग आप ऐप से पहले करते हैं (फ़ंक्शन) () {टेम्पलेट = angular.element ('');

        module('project');
        inject(function($injector, $controller, $rootScope, $compile, $templateCache) {
            $templateCache.put('partials/resources-list.html', jasmine.getFixtures().getFixtureHtml_('resources-list.html')); //loadFixture function doesn't return a string
            $scope = $rootScope.$new();
            $compile(template)($scope);
            $scope.$apply();
        })
    });

    });

  2. अपने ऐप के रूट डायरेक्टरी पर एक सर्वर चलाएं

    पायथन-एम सिंपलएचटीपीएसवर 3502

  3. कर्म चलाओ।

यह पता लगाने में मुझे थोड़ा समय लगा, कई पदों की खोज करने के बाद, मुझे लगता है कि इस बारे में प्रलेखन स्पष्ट होना चाहिए, क्योंकि यह इतना महत्वपूर्ण मुद्दा है।


मुझे संपत्तियों की सेवा करने में परेशानी हो रही थी localhost/base/specsऔर python -m SimpleHTTPServer 3502इसे तय करने के साथ एक प्रॉक्सी सर्वर जोड़ना था। आप सर एक जीनियस हैं!
pbojinov

मुझे अपने परीक्षणों में $ संकलन से खाली तत्व वापस मिल रहा था। अन्य स्थानों ने $ स्कोप चलाने का सुझाव दिया। $ डाइजेस्ट (): अभी भी खाली है। $ गुंजाइश चल रहा है। $ लागू होते हैं () हालांकि काम किया। मुझे लगता है कि यह इसलिए था क्योंकि मैं अपने निर्देश में एक नियंत्रक का उपयोग कर रहा हूं? निश्चित नहीं। सलाह के लिए धन्यवाद! मदद की!
सैम सीमन्स

7

मेरा समाधान:

test/karma-utils.js:

function httpGetSync(filePath) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/base/app/" + filePath, false);
  xhr.send();
  return xhr.responseText;
}

function preloadTemplate(path) {
  return inject(function ($templateCache) {
    var response = httpGetSync(path);
    $templateCache.put(path, response);
  });
}

karma.config.js:

files: [
  //(...)
  'test/karma-utils.js',
  'test/mock/**/*.js',
  'test/spec/**/*.js'
],

कसौटी:

'use strict';
describe('Directive: gowiliEvent', function () {
  // load the directive's module
  beforeEach(module('frontendSrcApp'));
  var element,
    scope;
  beforeEach(preloadTemplate('views/directives/event.html'));
  beforeEach(inject(function ($rootScope) {
    scope = $rootScope.$new();
  }));
  it('should exist', inject(function ($compile) {
    element = angular.element('<event></-event>');
    element = $compile(element)(scope);
    scope.$digest();
    expect(element.html()).toContain('div');
  }));
});

पहला सभ्य समाधान जो कर्मों का उपयोग करने के लिए देवताओं को मजबूर करने की कोशिश नहीं करता है। कोणीय लोग कुछ इतना बुरा और आसानी से कुछ के बीच में इतनी आसानी से परिहार्य क्यों करेंगे? pfff
Fabio Milheiro

मैं देखता हूं कि आप एक 'टेस्ट / मॉक / ** / *। Js' जोड़ते हैं और मुझे लगता है कि सेवाओं और सभी की तरह सभी नकली सामान को लोड करना है? मैं नकली सेवाओं के कोड दोहराव से बचने के तरीकों की तलाश कर रहा हूं। क्या आप हमें उस पर कुछ और दिखाएंगे?
स्टीफन

ठीक से याद नहीं है, लेकिन $ http सेवा के लिए JSONs उदाहरण के लिए संभावित सेटिंग्स थे। कुछ भी आकर्षक नहीं।
बर्थेक

आज यह समस्या थी - महान समाधान। हम कर्म का उपयोग करते हैं, लेकिन हम चुतजाह का भी उपयोग करते हैं - कोई कारण नहीं कि हमें कर्म का उपयोग करने के लिए मजबूर किया जाना चाहिए और केवल कर्म ही इकाई परीक्षण निर्देशों के लिए सक्षम होना चाहिए।
lwalden

हम Django का उपयोग Angular के साथ कर रहे हैं, और इसने एक निर्देश का परीक्षण करने के लिए एक आकर्षण की तरह काम किया जो अपने टेम्पलेट को लोड करता है हालांकि static, जैसे कि beforeEach(preloadTemplate(static_url +'seed/partials/beChartDropdown.html')); धन्यवाद!
अलेक लैंडग्राफ

6

यदि आप ग्रंट का उपयोग कर रहे हैं, तो आप ग्रंट-कोणीय-टेम्प्लेट का उपयोग कर सकते हैं। यह टेम्प्लेट कैश में आपके टेम्प्लेट को लोड करता है और यह आपके ऐनक कॉन्फ़िगरेशन के लिए पारदर्शी है।

मेरा नमूना विन्यास:

module.exports = function(grunt) {

  grunt.initConfig({

    pkg: grunt.file.readJSON('package.json'),

    ngtemplates: {
        myapp: {
          options: {
            base:       'public/partials',
            prepend:    'partials/',
            module:     'project'
          },
          src:          'public/partials/*.html',
          dest:         'spec/javascripts/angular/helpers/templates.js'
        }
    },

    watch: {
        templates: {
            files: ['public/partials/*.html'],
            tasks: ['ngtemplates']
        }
    }

  });

  grunt.loadNpmTasks('grunt-angular-templates');
  grunt.loadNpmTasks('grunt-contrib-watch');

};

6

मैंने उसी समस्या को चुने हुए समाधान की तुलना में थोड़ा अलग तरीके से हल किया।

  1. सबसे पहले, मैंने कर्म के लिए ng-html2js प्लगइन स्थापित और कॉन्फ़िगर किया । कर्म में .conf.js फ़ाइल:

    preprocessors: {
      'path/to/templates/**/*.html': 'ng-html2js'
    },
    ngHtml2JsPreprocessor: {
    // you might need to strip the main directory prefix in the URL request
      stripPrefix: 'path/'
    }
  2. फिर मैंने पहले वाले में बनाए गए मॉड्यूल को लोड किया। अपनी Spec.js फ़ाइल में:

    beforeEach(module('myApp', 'to/templates/myTemplate.html'));
  3. तब मैंने $ templateCache.get का उपयोग इसे एक चर में संग्रहीत करने के लिए किया था। अपनी Spec.js फ़ाइल में:

    var element,
        $scope,
        template;
    
    beforeEach(inject(function($rootScope, $compile, $templateCache) {
      $scope = $rootScope.$new();
      element = $compile('<div my-directive></div>')($scope);
      template = $templateCache.get('to/templates/myTemplate.html');
      $scope.$digest();
    }));
  4. अंत में, मैंने इसे इस तरह से परखा। अपनी Spec.js फ़ाइल में:

    describe('element', function() {
      it('should contain the template', function() {
        expect(element.html()).toMatch(template);
      });
    });

4

टेम्पलेट HTML को गतिशील रूप से $ टेम्पलेट में लोड करने के लिए आप बस html2js कर्म पूर्व-प्रोसेसर का उपयोग कर सकते हैं, जैसा कि यहां बताया गया है

यह टेम्प्लेट जोड़ने के लिए उबलता है ' .html' को आपकी फ़ाइलों में Js फाइल में प्रीप्रोसेसर = {' .html': 'html2js'};

और उपयोग करें

beforeEach(module('..'));

beforeEach(module('...html', '...html'));

अपने जे एस परीक्षण फ़ाइल में


मुझे मिल रहा हैUncaught SyntaxError: Unexpected token <
मेलबोर्न

2

यदि आप कर्म का उपयोग कर रहे हैं, तो अपने बाहरी HTML टेम्प्लेट को पूर्व-संकलित करने के लिए कर्म-एनजी- html2js-preprocessor का उपयोग करने पर विचार करें और परीक्षण निष्पादन के दौरान कोणीय HTTP HTTP को प्राप्त करने का प्रयास करने से बचें। मैं इस मामले में हमारे एक जोड़े के लिए संघर्ष करता रहा - मेरे मामले में टेम्पलेटयूएलआर के आंशिक रास्तों को सामान्य ऐप निष्पादन के दौरान हल किया गया, लेकिन परीक्षणों के दौरान नहीं - ऐप बनाम टेस्ट डीआईआर संरचनाओं में अंतर के कारण।


2

यदि आप आवश्यकताएँ के साथ एक साथ चमेली- मावेन -प्लगइन का उपयोग कर रहे हैं, तो आप टेम्प्लेट प्लगइन का उपयोग एक चर में टेम्पलेट सामग्री को लोड करने के लिए कर सकते हैं और फिर इसे टेम्पलेट कैश में डाल सकते हैं।


define(['angular', 'text!path/to/template.html', 'angular-route', 'angular-mocks'], function(ng, directiveTemplate) {
    "use strict";

    describe('Directive TestSuite', function () {

        beforeEach(inject(function( $templateCache) {
            $templateCache.put("path/to/template.html", directiveTemplate);
        }));

    });
});

क्या आप कर्म के बिना ऐसा कर सकते हैं?
विन्नमुक्का

2

यदि आप अपने परीक्षणों में रिक्जेस्ट का उपयोग करते हैं तो आप html टेम्प्लेट में खींचने के लिए 'टेक्स्ट' प्लगइन का उपयोग कर सकते हैं और इसे $ टेम्प्लेट कैश में डाल सकते हैं।

require(["text!template.html", "module-file"], function (templateHtml){
  describe("Thing", function () {

    var element, scope;

    beforeEach(module('module'));

    beforeEach(inject(function($templateCache, $rootScope, $compile){

      // VOILA!
      $templateCache.put('/path/to/the/template.html', templateHtml);  

      element = angular.element('<my-thing></my-thing>');
      scope = $rootScope;
      $compile(element)(scope);   

      scope.$digest();
    }));
  });
});

0

मैं इस मुद्दे को सभी टेम्प्लेट को टेम्पलेट के संकलन के साथ हल करता हूं। मैं गल्प का उपयोग कर रहा हूं, आप ग्रंट के लिए भी इसी तरह का समाधान पा सकते हैं। निर्देशों में मेरा टेम्पलेट, मॉडल्स जैसा दिखता है

`templateUrl: '/templates/directives/sidebar/tree.html'`
  1. मेरे पैकेज में नया npm पैकेज जोड़ें। json

    "gulp-angular-templatecache": "1.*"

  2. Gulp फ़ाइल में टेम्प्लेट और नया कार्य जोड़ें:

    var templateCache = require('gulp-angular-templatecache'); ... ... gulp.task('compileTemplates', function () { gulp.src([ './app/templates/**/*.html' ]).pipe(templateCache('templates.js', { transformUrl: function (url) { return '/templates/' + url; } })) .pipe(gulp.dest('wwwroot/assets/js')); });

  3. Index.html में सभी js फाइलें जोड़ें

    <script src="/assets/js/lib.js"></script> <script src="/assets/js/app.js"></script> <script src="/assets/js/templates.js"></script>

  4. का आनंद लें!

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.