diff --git a/tests/mocha/index.html b/tests/mocha/index.html
index 48c0c2e51..735eaa0bc 100644
--- a/tests/mocha/index.html
+++ b/tests/mocha/index.html
@@ -37,6 +37,7 @@
+
diff --git a/tests/mocha/trashcan_test.js b/tests/mocha/trashcan_test.js
new file mode 100644
index 000000000..d1ce4c36b
--- /dev/null
+++ b/tests/mocha/trashcan_test.js
@@ -0,0 +1,310 @@
+/**
+ * @license
+ * Visual Blocks Editor
+ *
+ * Copyright 2019 Google Inc.
+ * https://developers.google.com/blockly/
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+suite("Trashcan", function() {
+ var workspace = {
+ addChangeListener: function(func) {
+ this.listener = func;
+ },
+ triggerListener: function(event) {
+ this.listener(event);
+ },
+ options: {
+ maxTrashcanContents: Infinity
+ }
+ };
+ function sendDeleteEvent(xmlString) {
+ var xml = Blockly.Xml.textToDom('' + xmlString + '');
+ xml = xml.children[0];
+ var event = {
+ type: Blockly.Events.BLOCK_DELETE,
+ oldXml: xml
+ };
+ workspace.triggerListener(event);
+ }
+
+ setup(function() {
+ this.trashcan = new Blockly.Trashcan(workspace);
+ this.setLidStub = sinon.stub(this.trashcan, 'setLidAngle_');
+ });
+ teardown(function() {
+ this.setLidStub.restore();
+ this.trashcan = null;
+ });
+
+ suite("Events", function() {
+ test("Delete", function() {
+ sendDeleteEvent('');
+ chai.assert.equal(this.trashcan.contents_.length, 1);
+ });
+ test("Non-Delete", function() {
+ var event = {
+ type: 'dummy_type'
+ };
+ workspace.triggerListener(event);
+ chai.assert.equal(this.trashcan.contents_.length, 0);
+ });
+ test("Non-Delete w/ oldXml", function() {
+ var xml = Blockly.Xml.textToDom(
+ '' +
+ ' ' +
+ ''
+ );
+ xml = xml.children[0];
+ var event = {
+ type: 'dummy_type',
+ oldXml: xml
+ };
+ workspace.triggerListener(event);
+ chai.assert.equal(this.trashcan.contents_.length, 0);
+ });
+ test("Shadow Delete", function() {
+ sendDeleteEvent('');
+ chai.assert.equal(this.trashcan.contents_.length, 0);
+ });
+ });
+ suite("Unique Contents", function() {
+ test("Simple", function() {
+ sendDeleteEvent('');
+ sendDeleteEvent('');
+ chai.assert.equal(this.trashcan.contents_.length, 1);
+ });
+ test("Different Coords", function() {
+ sendDeleteEvent('');
+ sendDeleteEvent('');
+ chai.assert.equal(this.trashcan.contents_.length, 1);
+ });
+ test("Different IDs", function() {
+ sendDeleteEvent('');
+ sendDeleteEvent('');
+ chai.assert.equal(this.trashcan.contents_.length, 1);
+ });
+ test("No Disabled - Disabled True", function() {
+ sendDeleteEvent('');
+ sendDeleteEvent('');
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ test("No Editable - Editable False", function() {
+ sendDeleteEvent('');
+ sendDeleteEvent('');
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ test("No Movable - Movable False", function() {
+ sendDeleteEvent('');
+ sendDeleteEvent('');
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ test("Different Field Values", function() {
+ sendDeleteEvent(
+ '' +
+ ' dummy_value1' +
+ ''
+ );
+ sendDeleteEvent(
+ '' +
+ ' dummy_value2' +
+ ''
+ );
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ test("No Values - Values", function() {
+ sendDeleteEvent('');
+ sendDeleteEvent(
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ''
+ );
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ test("Different Value Blocks", function() {
+ sendDeleteEvent(
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ''
+ );
+ sendDeleteEvent(
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ''
+ );
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ test("No Statements - Statements", function() {
+ sendDeleteEvent('');
+ sendDeleteEvent(
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ''
+ );
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ test("Different Statement Blocks", function() {
+ sendDeleteEvent(
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ''
+ );
+ sendDeleteEvent(
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ''
+ );
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ test("No Next - Next", function() {
+ sendDeleteEvent('');
+ sendDeleteEvent(
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ''
+ );
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ test("Different Next Blocks", function() {
+ sendDeleteEvent(
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ''
+ );
+ sendDeleteEvent(
+ '' +
+ ' ' +
+ ' ' +
+ ' ' +
+ ''
+ );
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ test("No Comment - Comment", function() {
+ sendDeleteEvent('');
+ sendDeleteEvent(
+ '' +
+ ' comment_text' +
+ ''
+ );
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ test("Different Comment Text", function() {
+ sendDeleteEvent(
+ '' +
+ ' comment_text1' +
+ ''
+ );
+ sendDeleteEvent(
+ '' +
+ ' comment_text2' +
+ ''
+ );
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ test("Different Comment Size", function() {
+ sendDeleteEvent(
+ '' +
+ ' comment_text' +
+ ''
+ );
+ sendDeleteEvent(
+ '' +
+ ' comment_text' +
+ ''
+ );
+ // TODO (#2574): These blocks are treated as different, but appear
+ // identical when the trashcan is opened.
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ test("Different Comment Pinned", function() {
+ sendDeleteEvent(
+ '' +
+ ' comment_text' +
+ ''
+ );
+ sendDeleteEvent(
+ '' +
+ ' comment_text' +
+ ''
+ );
+ // TODO (#2574): These blocks are treated as different, but appear
+ // identical when the trashcan is opened.
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ test("No Mutator - Mutator", function() {
+ sendDeleteEvent('');
+ sendDeleteEvent(
+ '' +
+ ' ' +
+ ''
+ );
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ test("Different Mutator", function() {
+ sendDeleteEvent(
+ '' +
+ ' ' +
+ ''
+ );
+ sendDeleteEvent(
+ '' +
+ ' ' +
+ ''
+ );
+ chai.assert.equal(this.trashcan.contents_.length, 2);
+ });
+ });
+ suite("Max Contents", function() {
+ test("Max 0", function() {
+ workspace.options.maxTrashcanContents = 0;
+ sendDeleteEvent(
+ '' +
+ ' ' +
+ ''
+ );
+ chai.assert.equal(this.trashcan.contents_.length, 0);
+ workspace.options.maxTrashcanContents = Infinity;
+ });
+ test("Last In First Out", function() {
+ workspace.options.maxTrashcanContents = 1;
+ sendDeleteEvent('');
+ sendDeleteEvent('');
+ chai.assert.equal(this.trashcan.contents_.length, 1);
+ chai.assert.equal(
+ Blockly.Xml.textToDom(this.trashcan.contents_[0])
+ .children[0].getAttribute('type'),
+ 'dummy_type2'
+ );
+ workspace.options.maxTrashcanContents = Infinity;
+ });
+ });
+});