
polymer-resin intercepts polymer property assignments before they reach XSS-vulnerable sinks like `href="..."` and text nodes in `<script>` elements. This follows the instructions in WORKSPACE for adding a new bower dependency with kaspern's tweak to use the dependency in a rule so that it's found. //lib/js/bower_components.bzl has already been rolled-back per those instructions. The license is the polymer license as can be seen at https://github.com/Polymer/polymer-resin/blob/master/LICENSE though I'm not sure that //tools/js/bower2bazel.py recognizes it as such. Docs for the added component are available at https://github.com/Polymer/polymer-resin/blob/master/README.md https://github.com/Polymer/polymer-resin/blob/master/getting-started.md With this change, when I introduce an XSS vulnerability as below, polymer-resin intercepts and stops it. Patch that introduces a strawman vulnerability. --- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js +++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js @@ -55,6 +55,10 @@ url: '/q/status:abandoned', name: 'Abandoned', }, + { + url: location.hash.replace(/^#/, '') || 'http://example.com/#fragment_echoed_here', + name: 'XSS Me', + }, ], }]; --- Address kaspern's and paladox's comments. --- Undo version bumps for bower dependencies. --- Change Soy index template to parallel app/index.html. --- update polymer-resin to version 1.1.1-beta ---- Load polymer-resin into polygerrit-ui/**/*_test.html After this, I ran the tests with -l chrome -l firefox I ran a handful of tests with -p and observed that the console shows "initResin" is called before test cases start executing. These changes were done programmaticly by running the script below (approximately) thus: ``` gerrit/ $ cd polygerrit-ui/app app/ $ find . -name \*test.html | xargs perl hack-tests.pl ``` ``` use strict; sub removeResin($) { my $s = $_[0]; $s =~ s@<link rel="import" href="[^"]*/polymer-resin/[^"]*"[^>]*>\n?@@; $s =~ s@<script src="[^"]*/polymer-resin/[^"]*"></script>\n?@@; $s =~ s@<script>\s*security\.polymer_resin.*?</script>\n?@@s; return $s; } for my $f (@ARGV) { next if $f =~ m@/bower_components/|/node_modules/@; system('git', 'checkout', $f); print "$f\n"; my @lines = (); open(IN, "<$f") or die "$f: $!"; my $maxLineOfMatch = 0; while (<IN>) { push(@lines, $_); # Put a marker after core loading directives. $maxLineOfMatch = scalar(@lines) if m@/webcomponentsjs/|/polymer[.]html\b|/browser[.]js@; } close(IN) or die "$f: $!"; die "$f missing loading directives" unless $maxLineOfMatch; # Given ./a/b/c/my_test.html, $pathToRoot is "../../.." # assuming no non-leading . or .. components in the path from find. my $pathToRoot = $f; $pathToRoot =~ s@^\.\/@@; $pathToRoot =~ s@^(.*?/)?app/@@; $pathToRoot =~ s@\/[^\/]*$@@; $pathToRoot =~ s@[^/]+@..@g; my $nLines = scalar(@lines); open(OUT, ">$f") or die "$f: $!"; # Output the lines up to the last polymer-resin dependency # loaded explicitly by this test. my $before = join '', @lines[0..($maxLineOfMatch - 1)]; $before = removeResin($before); print OUT "$before"; # Dump out the lines that load polymer-resin and configure it for # polygerrit. if (1) { print OUT qq'<link rel="import" href="$pathToRoot/bower_components/polymer-resin/standalone/polymer-resin-debug.html"/> <script> security.polymer_resin.install({allowedIdentifierPrefixes: [\'\']}); </script> '; } # Emit any remaining lines. my $after = join '', @lines[$maxLineOfMatch..$#lines]; $after = removeResin($after); $after =~ s/^\n*//; print OUT "$after"; close(OUT) or die "$f: $!"; } ``` --- update polymer-resin to version 1.2.1-beta --- update Soy index template to new style polymer-resin initialization ---- fix lint warnings ---- Load test/common-test-setup.html into *_test.html Instead of inserting instructions to load and initialize polymer-resin into every test file, add a common-test-setup.html that does that and also fold iron-test-helpers loading into it. ---- imported files do not need to load webcomponentsjs Change-Id: I71221c36ed8a0fe7f8720c1064a2fcc9555bb8df
555 lines
18 KiB
HTML
555 lines
18 KiB
HTML
<!DOCTYPE html>
|
||
<!--
|
||
Copyright (C) 2015 The Android Open Source Project
|
||
|
||
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.
|
||
-->
|
||
|
||
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
|
||
<title>gr-diff-comment-thread</title>
|
||
|
||
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
|
||
<script src="../../../bower_components/web-component-tester/browser.js"></script>
|
||
<link rel="import" href="../../../test/common-test-setup.html"/>
|
||
<script src="../../../scripts/util.js"></script>
|
||
|
||
<link rel="import" href="gr-diff-comment-thread.html">
|
||
|
||
<script>void(0);</script>
|
||
|
||
<test-fixture id="basic">
|
||
<template>
|
||
<gr-diff-comment-thread></gr-diff-comment-thread>
|
||
</template>
|
||
</test-fixture>
|
||
|
||
<test-fixture id="withComment">
|
||
<template>
|
||
<gr-diff-comment-thread></gr-diff-comment-thread>
|
||
</template>
|
||
</test-fixture>
|
||
|
||
<script>
|
||
suite('gr-diff-comment-thread tests', () => {
|
||
let element;
|
||
let sandbox;
|
||
|
||
setup(() => {
|
||
sandbox = sinon.sandbox.create();
|
||
stub('gr-rest-api-interface', {
|
||
getLoggedIn() { return Promise.resolve(false); },
|
||
});
|
||
sandbox = sinon.sandbox.create();
|
||
element = fixture('basic');
|
||
});
|
||
|
||
teardown(() => {
|
||
sandbox.restore();
|
||
});
|
||
|
||
test('comments are sorted correctly', () => {
|
||
const comments = [
|
||
{
|
||
id: 'jacks_reply',
|
||
message: 'i like you, too',
|
||
in_reply_to: 'sallys_confession',
|
||
updated: '2015-12-25 15:00:20.396000000',
|
||
}, {
|
||
id: 'sallys_confession',
|
||
message: 'i like you, jack',
|
||
updated: '2015-12-24 15:00:20.396000000',
|
||
}, {
|
||
id: 'sally_to_dr_finklestein',
|
||
message: 'i’m running away',
|
||
updated: '2015-10-31 09:00:20.396000000',
|
||
}, {
|
||
id: 'sallys_defiance',
|
||
in_reply_to: 'sally_to_dr_finklestein',
|
||
message: 'i will poison you so i can get away',
|
||
updated: '2015-10-31 15:00:20.396000000',
|
||
}, {
|
||
id: 'dr_finklesteins_response',
|
||
in_reply_to: 'sally_to_dr_finklestein',
|
||
message: 'no i will pull a thread and your arm will fall off',
|
||
updated: '2015-10-31 11:00:20.396000000',
|
||
}, {
|
||
id: 'sallys_mission',
|
||
message: 'i have to find santa',
|
||
updated: '2015-12-24 15:00:20.396000000',
|
||
},
|
||
];
|
||
const results = element._sortedComments(comments);
|
||
assert.deepEqual(results, [
|
||
{
|
||
id: 'sally_to_dr_finklestein',
|
||
message: 'i’m running away',
|
||
updated: '2015-10-31 09:00:20.396000000',
|
||
}, {
|
||
id: 'dr_finklesteins_response',
|
||
in_reply_to: 'sally_to_dr_finklestein',
|
||
message: 'no i will pull a thread and your arm will fall off',
|
||
updated: '2015-10-31 11:00:20.396000000',
|
||
}, {
|
||
id: 'sallys_defiance',
|
||
in_reply_to: 'sally_to_dr_finklestein',
|
||
message: 'i will poison you so i can get away',
|
||
updated: '2015-10-31 15:00:20.396000000',
|
||
}, {
|
||
id: 'sallys_confession',
|
||
message: 'i like you, jack',
|
||
updated: '2015-12-24 15:00:20.396000000',
|
||
}, {
|
||
id: 'sallys_mission',
|
||
message: 'i have to find santa',
|
||
updated: '2015-12-24 15:00:20.396000000',
|
||
}, {
|
||
id: 'jacks_reply',
|
||
message: 'i like you, too',
|
||
in_reply_to: 'sallys_confession',
|
||
updated: '2015-12-25 15:00:20.396000000',
|
||
},
|
||
]);
|
||
});
|
||
|
||
test('addOrEditDraft w/ edit draft', () => {
|
||
element.comments = [{
|
||
id: 'jacks_reply',
|
||
message: 'i like you, too',
|
||
in_reply_to: 'sallys_confession',
|
||
updated: '2015-12-25 15:00:20.396000000',
|
||
__draft: true,
|
||
}];
|
||
const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
|
||
() => { return {}; });
|
||
const addDraftStub = sandbox.stub(element, 'addDraft');
|
||
|
||
element.addOrEditDraft(123);
|
||
|
||
assert.isTrue(commentElStub.called);
|
||
assert.isFalse(addDraftStub.called);
|
||
});
|
||
|
||
test('addOrEditDraft w/o edit draft', () => {
|
||
element.comments = [];
|
||
const commentElStub = sandbox.stub(element, '_commentElWithDraftID',
|
||
() => { return {}; });
|
||
const addDraftStub = sandbox.stub(element, 'addDraft');
|
||
|
||
element.addOrEditDraft(123);
|
||
|
||
assert.isFalse(commentElStub.called);
|
||
assert.isTrue(addDraftStub.called);
|
||
});
|
||
|
||
test('_hideActions', () => {
|
||
let showActions = true;
|
||
const lastComment = {};
|
||
assert.equal(element._hideActions(showActions, lastComment), false);
|
||
showActions = false;
|
||
assert.equal(element._hideActions(showActions, lastComment), true);
|
||
showActions = true;
|
||
lastComment.__draft = true;
|
||
assert.equal(element._hideActions(showActions, lastComment), true);
|
||
});
|
||
});
|
||
|
||
suite('comment action tests', () => {
|
||
let element;
|
||
|
||
setup(() => {
|
||
stub('gr-rest-api-interface', {
|
||
getLoggedIn() { return Promise.resolve(false); },
|
||
saveDiffDraft() {
|
||
return Promise.resolve({
|
||
ok: true,
|
||
text() {
|
||
return Promise.resolve(')]}\'\n' +
|
||
JSON.stringify({
|
||
id: '7afa4931_de3d65bd',
|
||
path: '/path/to/file.txt',
|
||
line: 5,
|
||
in_reply_to: 'baf0414d_60047215',
|
||
updated: '2015-12-21 02:01:10.850000000',
|
||
message: 'Done',
|
||
}));
|
||
},
|
||
});
|
||
},
|
||
deleteDiffDraft() { return Promise.resolve({ok: true}); },
|
||
});
|
||
element = fixture('withComment');
|
||
element.comments = [{
|
||
author: {
|
||
name: 'Mr. Peanutbutter',
|
||
email: 'tenn1sballchaser@aol.com',
|
||
},
|
||
id: 'baf0414d_60047215',
|
||
line: 5,
|
||
message: 'is this a crossover episode!?',
|
||
updated: '2015-12-08 19:48:33.843000000',
|
||
}];
|
||
flushAsynchronousOperations();
|
||
});
|
||
|
||
test('reply', done => {
|
||
const commentEl = element.$$('gr-diff-comment');
|
||
assert.ok(commentEl);
|
||
|
||
const replyBtn = element.$.replyBtn;
|
||
MockInteractions.tap(replyBtn);
|
||
flushAsynchronousOperations();
|
||
|
||
const drafts = element._orderedComments.filter(c => {
|
||
return c.__draft == true;
|
||
});
|
||
assert.equal(drafts.length, 1);
|
||
assert.notOk(drafts[0].message, 'message should be empty');
|
||
assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
|
||
done();
|
||
});
|
||
|
||
test('quote reply', done => {
|
||
const commentEl = element.$$('gr-diff-comment');
|
||
assert.ok(commentEl);
|
||
|
||
const quoteBtn = element.$.quoteBtn;
|
||
MockInteractions.tap(quoteBtn);
|
||
flushAsynchronousOperations();
|
||
|
||
const drafts = element._orderedComments.filter(c => {
|
||
return c.__draft == true;
|
||
});
|
||
assert.equal(drafts.length, 1);
|
||
assert.equal(drafts[0].message, '> is this a crossover episode!?\n\n');
|
||
assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
|
||
done();
|
||
});
|
||
|
||
test('quote reply multiline', done => {
|
||
element.comments = [{
|
||
author: {
|
||
name: 'Mr. Peanutbutter',
|
||
email: 'tenn1sballchaser@aol.com',
|
||
},
|
||
id: 'baf0414d_60047215',
|
||
line: 5,
|
||
message: 'is this a crossover episode!?\nIt might be!',
|
||
updated: '2015-12-08 19:48:33.843000000',
|
||
}];
|
||
flushAsynchronousOperations();
|
||
|
||
const commentEl = element.$$('gr-diff-comment');
|
||
assert.ok(commentEl);
|
||
|
||
const quoteBtn = element.$.quoteBtn;
|
||
MockInteractions.tap(quoteBtn);
|
||
flushAsynchronousOperations();
|
||
|
||
const drafts = element._orderedComments.filter(c => {
|
||
return c.__draft == true;
|
||
});
|
||
assert.equal(drafts.length, 1);
|
||
assert.equal(drafts[0].message,
|
||
'> is this a crossover episode!?\n> It might be!\n\n');
|
||
assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
|
||
done();
|
||
});
|
||
|
||
test('ack', done => {
|
||
element.changeNum = '42';
|
||
element.patchNum = '1';
|
||
|
||
const commentEl = element.$$('gr-diff-comment');
|
||
assert.ok(commentEl);
|
||
|
||
const ackBtn = element.$.ackBtn;
|
||
MockInteractions.tap(ackBtn);
|
||
flush(() => {
|
||
const drafts = element.comments.filter(c => {
|
||
return c.__draft == true;
|
||
});
|
||
assert.equal(drafts.length, 1);
|
||
assert.equal(drafts[0].message, 'Ack');
|
||
assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
|
||
assert.equal(drafts[0].unresolved, false);
|
||
done();
|
||
});
|
||
});
|
||
|
||
test('done', done => {
|
||
element.changeNum = '42';
|
||
element.patchNum = '1';
|
||
const commentEl = element.$$('gr-diff-comment');
|
||
assert.ok(commentEl);
|
||
|
||
const doneBtn = element.$.doneBtn;
|
||
MockInteractions.tap(doneBtn);
|
||
flush(() => {
|
||
const drafts = element.comments.filter(c => {
|
||
return c.__draft == true;
|
||
});
|
||
assert.equal(drafts.length, 1);
|
||
assert.equal(drafts[0].message, 'Done');
|
||
assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
|
||
assert.isFalse(drafts[0].unresolved);
|
||
done();
|
||
});
|
||
});
|
||
|
||
test('please fix', done => {
|
||
element.changeNum = '42';
|
||
element.patchNum = '1';
|
||
const commentEl = element.$$('gr-diff-comment');
|
||
assert.ok(commentEl);
|
||
commentEl.addEventListener('create-fix-comment', () => {
|
||
const drafts = element._orderedComments.filter(c => {
|
||
return c.__draft == true;
|
||
});
|
||
assert.equal(drafts.length, 1);
|
||
assert.equal(
|
||
drafts[0].message, '> is this a crossover episode!?\n\nPlease Fix');
|
||
assert.equal(drafts[0].in_reply_to, 'baf0414d_60047215');
|
||
assert.isTrue(drafts[0].unresolved);
|
||
done();
|
||
});
|
||
commentEl.fire('create-fix-comment', {comment: commentEl.comment},
|
||
{bubbles: false});
|
||
});
|
||
|
||
test('discard', done => {
|
||
element.changeNum = '42';
|
||
element.patchNum = '1';
|
||
element.push('comments', element._newReply(
|
||
element.comments[0].id,
|
||
element.comments[0].line,
|
||
element.comments[0].path,
|
||
'it’s pronouced jiff, not giff'));
|
||
flushAsynchronousOperations();
|
||
|
||
const draftEl =
|
||
Polymer.dom(element.root).querySelectorAll('gr-diff-comment')[1];
|
||
assert.ok(draftEl);
|
||
draftEl.addEventListener('comment-discard', () => {
|
||
const drafts = element.comments.filter(c => {
|
||
return c.__draft == true;
|
||
});
|
||
assert.equal(drafts.length, 0);
|
||
done();
|
||
});
|
||
draftEl.fire('comment-discard', null, {bubbles: false});
|
||
});
|
||
|
||
test('first editing comment does not add __otherEditing attribute', () => {
|
||
element.comments = [{
|
||
author: {
|
||
name: 'Mr. Peanutbutter',
|
||
email: 'tenn1sballchaser@aol.com',
|
||
},
|
||
id: 'baf0414d_60047215',
|
||
line: 5,
|
||
message: 'is this a crossover episode!?',
|
||
updated: '2015-12-08 19:48:33.843000000',
|
||
__draft: true,
|
||
}];
|
||
|
||
const replyBtn = element.$.replyBtn;
|
||
MockInteractions.tap(replyBtn);
|
||
flushAsynchronousOperations();
|
||
|
||
const editing = element._orderedComments.filter(c => {
|
||
return c.__editing == true;
|
||
});
|
||
assert.equal(editing.length, 1);
|
||
assert.equal(!!editing[0].__otherEditing, false);
|
||
});
|
||
|
||
test('When not editing other comments, local storage not set' +
|
||
' after discard', done => {
|
||
element.changeNum = '42';
|
||
element.patchNum = '1';
|
||
element.comments = [{
|
||
author: {
|
||
name: 'Mr. Peanutbutter',
|
||
email: 'tenn1sballchaser@aol.com',
|
||
},
|
||
id: 'baf0414d_60047215',
|
||
line: 5,
|
||
message: 'is this a crossover episode!?',
|
||
updated: '2015-12-08 19:48:31.843000000',
|
||
},
|
||
{
|
||
author: {
|
||
name: 'Mr. Peanutbutter',
|
||
email: 'tenn1sballchaser@aol.com',
|
||
},
|
||
__draftID: '1',
|
||
in_reply_to: 'baf0414d_60047215',
|
||
line: 5,
|
||
message: 'yes',
|
||
updated: '2015-12-08 19:48:32.843000000',
|
||
__draft: true,
|
||
__editing: true,
|
||
},
|
||
{
|
||
author: {
|
||
name: 'Mr. Peanutbutter',
|
||
email: 'tenn1sballchaser@aol.com',
|
||
},
|
||
__draftID: '2',
|
||
in_reply_to: 'baf0414d_60047215',
|
||
line: 5,
|
||
message: 'no',
|
||
updated: '2015-12-08 19:48:33.843000000',
|
||
__draft: true,
|
||
}];
|
||
const storageStub = sinon.stub(element.$.storage, 'setDraftComment');
|
||
flushAsynchronousOperations();
|
||
|
||
const draftEl =
|
||
Polymer.dom(element.root).querySelectorAll('gr-diff-comment')[1];
|
||
assert.ok(draftEl);
|
||
draftEl.addEventListener('comment-discard', () => {
|
||
assert.isFalse(storageStub.called);
|
||
storageStub.restore();
|
||
done();
|
||
});
|
||
draftEl.fire('comment-discard', null, {bubbles: false});
|
||
});
|
||
|
||
test('comment-update', () => {
|
||
const commentEl = element.$$('gr-diff-comment');
|
||
const updatedComment = {
|
||
id: element.comments[0].id,
|
||
foo: 'bar',
|
||
};
|
||
commentEl.fire('comment-update', {comment: updatedComment});
|
||
assert.strictEqual(element.comments[0], updatedComment);
|
||
});
|
||
|
||
suite('jack and sally comment data test consolidation', () => {
|
||
setup(() => {
|
||
element.comments = [
|
||
{
|
||
id: 'jacks_reply',
|
||
message: 'i like you, too',
|
||
in_reply_to: 'sallys_confession',
|
||
updated: '2015-12-25 15:00:20.396000000',
|
||
unresolved: false,
|
||
}, {
|
||
id: 'sallys_confession',
|
||
in_reply_to: 'nonexistent_comment',
|
||
message: 'i like you, jack',
|
||
updated: '2015-12-24 15:00:20.396000000',
|
||
}, {
|
||
id: 'sally_to_dr_finklestein',
|
||
in_reply_to: 'nonexistent_comment',
|
||
message: 'i’m running away',
|
||
updated: '2015-10-31 09:00:20.396000000',
|
||
}, {
|
||
id: 'sallys_defiance',
|
||
message: 'i will poison you so i can get away',
|
||
updated: '2015-10-31 15:00:20.396000000',
|
||
}];
|
||
});
|
||
|
||
test('orphan replies', () => {
|
||
assert.equal(4, element._orderedComments.length);
|
||
});
|
||
|
||
test('keyboard shortcuts', () => {
|
||
const expandCollapseStub =
|
||
sinon.stub(element, '_expandCollapseComments');
|
||
MockInteractions.pressAndReleaseKeyOn(element, 69, null, 'e');
|
||
assert.isTrue(expandCollapseStub.lastCall.calledWith(false));
|
||
|
||
MockInteractions.pressAndReleaseKeyOn(element, 69, 'shift', 'e');
|
||
assert.isTrue(expandCollapseStub.lastCall.calledWith(true));
|
||
});
|
||
|
||
test('comment in_reply_to is either null or most recent comment', () => {
|
||
element._createReplyComment(element.comments[3], 'dummy', true);
|
||
flushAsynchronousOperations();
|
||
assert.equal(element._orderedComments.length, 5);
|
||
assert.equal(element._orderedComments[4].in_reply_to, 'jacks_reply');
|
||
});
|
||
|
||
test('resolvable comments', () => {
|
||
assert.isFalse(element._unresolved);
|
||
element._createReplyComment(element.comments[3], 'dummy', true, true);
|
||
flushAsynchronousOperations();
|
||
assert.isTrue(element._unresolved);
|
||
});
|
||
|
||
test('_setInitialExpandedState', () => {
|
||
element._unresolved = true;
|
||
element._setInitialExpandedState();
|
||
for (let i = 0; i < element.comments.length; i++) {
|
||
assert.isFalse(element.comments[i].collapsed);
|
||
}
|
||
element._unresolved = false;
|
||
element._setInitialExpandedState();
|
||
for (let i = 0; i < element.comments.length; i++) {
|
||
assert.isTrue(element.comments[i].collapsed);
|
||
}
|
||
});
|
||
});
|
||
|
||
test('_computeHostClass', () => {
|
||
assert.equal(element._computeHostClass(true), 'unresolved');
|
||
assert.equal(element._computeHostClass(false), '');
|
||
});
|
||
|
||
test('addDraft sets unresolved state correctly', () => {
|
||
let unresolved = true;
|
||
element.comments = [];
|
||
element.addDraft(null, null, unresolved);
|
||
assert.equal(element.comments[0].unresolved, true);
|
||
|
||
unresolved = false; // comment should get added as actually resolved.
|
||
element.comments = [];
|
||
element.addDraft(null, null, unresolved);
|
||
assert.equal(element.comments[0].unresolved, false);
|
||
|
||
element.comments = [];
|
||
element.addDraft();
|
||
assert.equal(element.comments[0].unresolved, true);
|
||
});
|
||
|
||
test('_newDraft', () => {
|
||
element.commentSide = 'left';
|
||
element.patchNum = 3;
|
||
const draft = element._newDraft();
|
||
assert.equal(draft.__commentSide, 'left');
|
||
assert.equal(draft.patchNum, 3);
|
||
});
|
||
|
||
test('new comment gets created', () => {
|
||
element.comments = [];
|
||
element.addOrEditDraft(1);
|
||
assert.equal(element.comments.length, 1);
|
||
// Mock a submitted comment.
|
||
element.comments[0].id = element.comments[0].__draftID;
|
||
element.comments[0].__draft = false;
|
||
element.addOrEditDraft(1);
|
||
assert.equal(element.comments.length, 2);
|
||
});
|
||
|
||
test('unresolved label', () => {
|
||
element._unresolved = false;
|
||
assert.isTrue(element.$.unresolvedLabel.hasAttribute('hidden'));
|
||
element._unresolved = true;
|
||
assert.isFalse(element.$.unresolvedLabel.hasAttribute('hidden'));
|
||
});
|
||
});
|
||
</script>
|