mirror of
https://github.com/alexadam/save-as-ebook.git
synced 2026-10-21 05:06:02 +00:00
406 lines
19 KiB
HTML
406 lines
19 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<link rel="stylesheet" href="../../web-extension/chapterEditor.css">
|
|
<link rel="stylesheet" href="../../web-extension/cssEditor.css">
|
|
</head>
|
|
<body>
|
|
<script>
|
|
// Stand in for the extension apis the editors use on their own page
|
|
var FIXTURE_PAGES = [
|
|
{title: 'Chapter One', url: 'c1.xhtml', content: '<div><p>one</p></div>', images: [], removed: false},
|
|
{title: 'Chapter Two', url: 'c2.xhtml', content: '<div><p>two</p></div>', images: [], removed: false}
|
|
];
|
|
var sent = [];
|
|
// what was written, so a build can be checked against what the page held rather
|
|
// than against what storage would have answered with
|
|
var written = {pages: null, title: null};
|
|
// What the background answers a write with. A refusal is the shape the editor
|
|
// has to be able to tell from a success: chrome.storage runs the callback
|
|
// either way, so an answer is the only thing that carries the difference.
|
|
var refuseWrites = false;
|
|
|
|
window.chrome = {
|
|
runtime: {
|
|
onMessage: {addListener: function(){}},
|
|
lastError: null,
|
|
getURL: function(p){ return 'chrome-extension://fake/' + p; },
|
|
sendMessage: function(msg, cb) {
|
|
sent.push(msg.type);
|
|
var reply = {};
|
|
if (msg.type.indexOf('set') === 0 || msg.type === 'remove') {
|
|
reply = refuseWrites ? {ok: false, error: 'QUOTA_BYTES quota exceeded'} : {ok: true};
|
|
}
|
|
// a refused write stores nothing, which is the half of it the page cannot
|
|
// see for itself
|
|
if (msg.type === 'set' && !refuseWrites) written.pages = msg.pages;
|
|
if (msg.type === 'set title' && !refuseWrites) written.title = msg.title;
|
|
if (msg.type === 'get') reply = {allPages: FIXTURE_PAGES};
|
|
if (msg.type === 'get title') reply = {title: 'My Book'};
|
|
if (msg.type === 'get uuid') reply = {uuid: 'fixture-uuid'};
|
|
if (msg.type === 'get include style') reply = {includeStyle: true};
|
|
if (cb) setTimeout(function(){ cb(reply); }, 0);
|
|
}
|
|
},
|
|
i18n: {getMessage: function(k){ return k; }},
|
|
tabs: {query: function(q, cb){ cb([{id: 1, url: 'https://example.com'}]); },
|
|
create: function(){}, update: function(){}},
|
|
commands: {getAll: function(cb){ cb([]); }}
|
|
};
|
|
|
|
var errors = [];
|
|
window.addEventListener('error', function(e){ errors.push(e.message); });
|
|
</script>
|
|
|
|
<!-- the script set chapters.html loads, in the same order -->
|
|
<script src="../../web-extension/utils.js"></script>
|
|
<script src="../../web-extension/libs/jszip.js"></script>
|
|
<script src="../../web-extension/sanitizeHtml.js"></script>
|
|
<script src="../../web-extension/cssSanitizer.js"></script>
|
|
<script src="../../web-extension/saveEbook.js"></script>
|
|
<script src="../../web-extension/chapterPreview.js"></script>
|
|
<script src="../../web-extension/chapterContentEditor.js"></script>
|
|
<script src="../../web-extension/chapterEditor.js"></script>
|
|
|
|
<script>
|
|
// A write from this page is a message and the answer to it, and the fixture's
|
|
// stub answers on a timer - so what a save leads to (the confirmation it prints,
|
|
// the close that follows, the dirty baseline it resets, the unload guard that
|
|
// comes back after the close) is several turns of the event loop away from the
|
|
// click. The steps below wait for that rather than asserting into the middle of
|
|
// it: the editor now says a book was saved when the writes come back, which is
|
|
// the whole point of the change these tests are about.
|
|
function afterWrites(fn) {
|
|
setTimeout(fn, 50);
|
|
}
|
|
|
|
function report(name, ok, detail) {
|
|
var li = document.createElement('li');
|
|
li.textContent = (ok ? 'PASS ' : 'FAIL ') + name + (detail ? ' -- ' + detail : '');
|
|
document.getElementById('results').appendChild(li);
|
|
}
|
|
|
|
setTimeout(function () {
|
|
var out = document.createElement('ul');
|
|
out.id = 'results';
|
|
document.body.appendChild(out);
|
|
|
|
report('chapterEditor script ran without throwing', errors.length === 0, errors.join(' | '));
|
|
report('editor modal rendered', !!document.getElementById('chapterEditor-Modal'));
|
|
report('ebook title loaded from storage',
|
|
(document.getElementById('chapterEditor-ebookTitle') || {}).value === 'My Book');
|
|
var items = document.getElementsByClassName('chapterEditor-chapter-item');
|
|
report('both chapters listed', items.length === 2, 'found ' + items.length);
|
|
report('editor css not disabled',
|
|
Array.prototype.every.call(document.styleSheets, function (s) { return !s.disabled; }));
|
|
report('page stylesheets left enabled (no global disable)',
|
|
document.styleSheets.length > 0);
|
|
|
|
// ---- generating builds from the chapters the page is holding ----
|
|
//
|
|
// Not from storage: saveChanges() has only just started writing them, and
|
|
// that write is asynchronous. The editor used to reread storage here and
|
|
// race its own write.
|
|
var built = null;
|
|
buildEbook = function (pages, bookMeta) { built = {pages: pages, meta: bookMeta}; };
|
|
|
|
document.getElementById('text0').value = 'Renamed One';
|
|
document.getElementById('chapterEditor-ebookTitle').value = 'Edited Book';
|
|
// whatever storage would answer with now is stale by construction
|
|
FIXTURE_PAGES = [{title: 'Stale', url: 'stale.xhtml', content: '', images: []}];
|
|
sent = [];
|
|
document.getElementsByClassName('chapterEditor-generate-button')[0].click();
|
|
|
|
setTimeout(function () {
|
|
report('the build was handed the chapters, not left to find them', !!built);
|
|
report('both chapters reached the build', !!built && built.pages.length === 2,
|
|
built ? 'got ' + built.pages.length : 'no build');
|
|
report('the build sees the rename that was just made',
|
|
!!built && built.pages[0].title === 'Renamed One',
|
|
built ? built.pages[0].title : 'no build');
|
|
report('the build did not reread storage',
|
|
sent.indexOf('get') < 0, sent.join(','));
|
|
report('what was built is what was written',
|
|
!!built && written.pages === built.pages,
|
|
written.pages ? 'wrote ' + written.pages.length : 'nothing written');
|
|
report('the edited book title reaches the build',
|
|
!!built && built.meta.title === 'Edited Book' && written.title === 'Edited Book',
|
|
built ? built.meta.title : 'no build');
|
|
// the one thing that does have to come from storage: nothing on this page
|
|
// writes the identifier
|
|
report('the stored identifier reaches the build',
|
|
!!built && built.meta.uuid === 'fixture-uuid',
|
|
built ? String(built.meta.uuid) : 'no build');
|
|
|
|
// ---- a title box that has been emptied ----
|
|
//
|
|
// The build has a fallback for a chapter that reaches it without a title,
|
|
// but a box on the screen with nothing in it is a rename in progress: the
|
|
// page says which row it is and puts the cursor back in it rather than
|
|
// publishing a book with a chapter called "Untitled chapter 1".
|
|
var blankAlert = null;
|
|
window.alert = function (text) { blankAlert = text; };
|
|
built = null;
|
|
written.pages = null;
|
|
written.title = null;
|
|
var emptiedBox = document.getElementById('text0');
|
|
emptiedBox.value = ' ';
|
|
document.getElementsByClassName('chapterEditor-generate-button')[0].click();
|
|
|
|
report('generating with an empty chapter title says so',
|
|
blankAlert === 'blankChapterTitleWarning', String(blankAlert));
|
|
report('an empty chapter title builds nothing', built === null,
|
|
built ? 'built ' + built.pages.length + ' chapters' : '');
|
|
report('an empty chapter title writes nothing', written.pages === null,
|
|
written.pages ? 'wrote ' + written.pages.length : '');
|
|
report('the cursor goes back to the box that was emptied',
|
|
document.activeElement === emptiedBox,
|
|
document.activeElement ? String(document.activeElement.id) : 'nothing focused');
|
|
emptiedBox.value = 'Renamed One';
|
|
|
|
// ---- saving changes says so and leaves ----
|
|
//
|
|
// The tab is the editor, so a save that closed silently would look exactly
|
|
// like one that never happened. The alert is also what keeps the close
|
|
// safe: it holds the tab open while the writes behind it are delivered.
|
|
var alerted = null;
|
|
var closed = 0;
|
|
window.alert = function (text) { alerted = text; };
|
|
window.close = function () { closed++; };
|
|
|
|
written.pages = null;
|
|
written.title = null;
|
|
document.getElementById('text1').value = 'Renamed Two';
|
|
document.getElementById('chapterEditor-ebookTitle').value = 'Saved Book';
|
|
|
|
var saveChangesButton = null;
|
|
var footerButtons = document.getElementById('chapterEditor-modalFooter')
|
|
.getElementsByTagName('button');
|
|
for (var i = 0; i < footerButtons.length; i++) {
|
|
if (footerButtons[i].innerText === 'saveChanges') {
|
|
saveChangesButton = footerButtons[i];
|
|
}
|
|
}
|
|
report('the save changes button is there', !!saveChangesButton);
|
|
if (saveChangesButton) {
|
|
saveChangesButton.click();
|
|
}
|
|
|
|
// Everything a save leads to - the confirmation, the close it makes, the
|
|
// guard that comes back after the close - now waits for the four writes to
|
|
// be answered, so the steps that assert on it wait too.
|
|
afterWrites(function () {
|
|
report('saving wrote what the page shows',
|
|
!!written.pages && written.pages.length === 2 &&
|
|
written.pages[1].title === 'Renamed Two' &&
|
|
written.title === 'Saved Book',
|
|
written.pages ? written.pages.length + ' chapters, title ' +
|
|
String(written.title) : 'nothing written');
|
|
report('saving confirms it saved', alerted === 'changesSaved',
|
|
String(alerted));
|
|
report('the editor closes after the confirmation', closed === 1,
|
|
'closed ' + closed + ' times');
|
|
|
|
// ---- removing a chapter asks first ----
|
|
//
|
|
// A row's Remove button takes the chapter out with nothing to undo it, so
|
|
// it has to be a question rather than a click away from losing a chapter.
|
|
var confirmCalls = 0;
|
|
var asked = null;
|
|
var confirmAnswer = false;
|
|
window.confirm = function (text) { confirmCalls++; asked = text; return confirmAnswer; };
|
|
|
|
var firstRow = document.getElementById('li0');
|
|
var removeButton = firstRow.getElementsByTagName('button')[1];
|
|
removeButton.click();
|
|
report('removing a chapter asks first', confirmCalls === 1,
|
|
'asked ' + confirmCalls + ' times');
|
|
report('the question names the chapter as the row shows it',
|
|
String(asked).indexOf('Renamed One') > -1, String(asked));
|
|
report('answering no keeps the chapter', firstRow.style.display !== 'none',
|
|
'display ' + String(firstRow.style.display));
|
|
|
|
confirmAnswer = true;
|
|
removeButton.click();
|
|
report('answering yes removes the chapter', firstRow.style.display === 'none',
|
|
'display ' + String(firstRow.style.display));
|
|
|
|
written.pages = null;
|
|
if (saveChangesButton) {
|
|
saveChangesButton.click();
|
|
}
|
|
report('the removed chapter is not saved',
|
|
!!written.pages && written.pages.length === 1 &&
|
|
written.pages[0].title === 'Renamed Two',
|
|
written.pages ? written.pages.length + ' chapters' : 'nothing written');
|
|
|
|
// Everything below happens after that save has landed, the way a user
|
|
// does it: the save above closed the editor, and the unload guard it
|
|
// stood down for that close comes back once the close has had its chance.
|
|
afterWrites(function () {
|
|
// ---- closing the editor does not discard the book ----
|
|
//
|
|
// Everything on this page - the book title, the stylesheet, the metadata
|
|
// boxes, the renames, the order and what is marked removed - lives here
|
|
// until something writes it. Escape, a click on the grey margin either side
|
|
// of the modal and the X are all one keystroke or one stray click, and each
|
|
// of them closes the tab.
|
|
var upperClose = document.getElementById('chapterEditor-modalHeader')
|
|
.getElementsByTagName('button')[0];
|
|
var modalEl = document.getElementById('chapterEditor-Modal');
|
|
var chapterList = document.getElementsByClassName('chapterEditor-chapters-list')[0];
|
|
var asked = [];
|
|
var answerClose = false;
|
|
window.confirm = function (text) { asked.push(text); return answerClose; };
|
|
|
|
function firesBeforeUnload() {
|
|
var evt = new Event('beforeunload', {cancelable: true});
|
|
window.dispatchEvent(evt);
|
|
return evt.defaultPrevented;
|
|
}
|
|
|
|
function pressEscape() {
|
|
document.onkeydown({keyCode: 27});
|
|
}
|
|
|
|
function clickBackdrop() {
|
|
// the grey margin either side of the 70%-wide content: the event's target
|
|
// is the modal itself, which is what the page's own handler tests for
|
|
modalEl.click();
|
|
}
|
|
|
|
// nothing has changed since the save above
|
|
report('an unchanged editor is not holding anything back',
|
|
firesBeforeUnload() === false);
|
|
|
|
// ---- a rename that has not been saved ----
|
|
asked = [];
|
|
closed = 0;
|
|
document.getElementById('text1').value = 'Renamed Again';
|
|
report('an unsaved rename stops the tab from being closed from the browser',
|
|
firesBeforeUnload() === true);
|
|
|
|
pressEscape();
|
|
report('escape with unsaved changes asks first', asked.length === 1, asked.join(' | '));
|
|
report('answering no to escape keeps the tab open', closed === 0,
|
|
'closed ' + closed + ' times');
|
|
|
|
clickBackdrop();
|
|
report('a backdrop click with unsaved changes asks first', asked.length === 2,
|
|
asked.join(' | '));
|
|
report('answering no to the backdrop keeps the tab open', closed === 0,
|
|
'closed ' + closed + ' times');
|
|
|
|
upperClose.click();
|
|
report('the X with unsaved changes asks first', asked.length === 3, asked.join(' | '));
|
|
report('answering no to the X keeps the tab open', closed === 0,
|
|
'closed ' + closed + ' times');
|
|
report('the question is about the book, not about one chapter',
|
|
asked[2] === 'discardChangesConfirm', String(asked[2]));
|
|
|
|
// ---- everything else the page holds counts as work too ----
|
|
document.getElementById('text1').value = 'Renamed Two';
|
|
report('putting the title back makes the editor clean again',
|
|
firesBeforeUnload() === false);
|
|
|
|
document.getElementById('chapterEditor-bookCss').value = 'p { margin: 0 }';
|
|
report('an unsaved book stylesheet counts', firesBeforeUnload() === true);
|
|
document.getElementById('chapterEditor-bookCss').value = '';
|
|
|
|
document.getElementById('chapterEditor-bookMeta-publisher').value = 'A Publisher';
|
|
report('unsaved book metadata counts', firesBeforeUnload() === true);
|
|
document.getElementById('chapterEditor-bookMeta-publisher').value = '';
|
|
|
|
document.getElementById('chapterEditor-ebookTitle').value = 'Another Title';
|
|
report('an unsaved book title counts', firesBeforeUnload() === true);
|
|
document.getElementById('chapterEditor-ebookTitle').value = 'Saved Book';
|
|
|
|
chapterList.appendChild(chapterList.children[0]);
|
|
report('an unsaved reorder counts', firesBeforeUnload() === true);
|
|
chapterList.insertBefore(chapterList.children[1], chapterList.children[0]);
|
|
report('putting the order back makes the editor clean again',
|
|
firesBeforeUnload() === false);
|
|
|
|
// ---- a row marked removed is a change too ----
|
|
//
|
|
// Nothing writes it until the book is saved, and the row simply stops
|
|
// being drawn - so of everything this page holds, this is the change that
|
|
// is least visible and costs a whole chapter.
|
|
asked = [];
|
|
answerClose = true; // the row's own "remove this chapter?" question
|
|
document.getElementById('li1').getElementsByTagName('button')[1].click();
|
|
answerClose = false;
|
|
report('the row asked before marking the chapter removed', asked.length === 1,
|
|
asked.join(' | '));
|
|
report('a chapter marked removed counts as unsaved work',
|
|
firesBeforeUnload() === true);
|
|
// Generate rather than Save changes: it writes the same things, and it is
|
|
// the one that leaves the tab open to go on asserting against.
|
|
document.getElementsByClassName('chapterEditor-generate-button')[0].click();
|
|
// the reset of the baseline is a write coming back - see afterWrites
|
|
afterWrites(function () {
|
|
report('saving the removal makes the editor clean again',
|
|
firesBeforeUnload() === false);
|
|
|
|
// ---- a close the user agreed to, and one there is nothing to ask about ----
|
|
asked = [];
|
|
closed = 0;
|
|
upperClose.click();
|
|
report('closing an unchanged editor asks nothing', asked.length === 0,
|
|
asked.join(' | '));
|
|
report('closing an unchanged editor closes', closed === 1,
|
|
'closed ' + closed + ' times');
|
|
// a change made after the click, while the tab is on its way out: the
|
|
// guard is stood down for a close the user has already been through
|
|
document.getElementById('text1').value = 'Changed On The Way Out';
|
|
report('a close it has already agreed to does not ask again on the way out',
|
|
firesBeforeUnload() === false);
|
|
|
|
setTimeout(function () {
|
|
// ...and comes back for a tab the browser declined to close, rather
|
|
// than leaving the book unguarded for the rest of its life
|
|
report('a tab that did not actually close is guarded again',
|
|
firesBeforeUnload() === true);
|
|
|
|
// ---- and the answer that does discard ----
|
|
asked = [];
|
|
closed = 0;
|
|
answerClose = true;
|
|
upperClose.click();
|
|
report('answering yes discards the changes and closes',
|
|
asked.length === 1 && closed === 1,
|
|
'asked ' + asked.length + ', closed ' + closed);
|
|
|
|
// ---- a save the profile refuses ----
|
|
//
|
|
// chrome.storage.local.set runs its callback whether or not it wrote
|
|
// anything, so this page used to say "changes saved" and close the
|
|
// tab on a book that had not been stored. What it says now is
|
|
// decided by the answers to its four writes.
|
|
refuseWrites = true;
|
|
alerted = null;
|
|
closed = 0;
|
|
document.getElementById('text1').value = 'A Rename Nothing Will Keep';
|
|
saveChangesButton.click();
|
|
|
|
afterWrites(function () {
|
|
report('a refused save says so instead of saying it saved',
|
|
alerted === 'storageWriteFailed', String(alerted));
|
|
report('a refused save leaves the editor open',
|
|
closed === 0, 'closed ' + closed + ' times');
|
|
// the baseline is not reset by a write that did not land, so the
|
|
// work is still work and the tab still says so on the way out
|
|
report('a refused save leaves the changes unsaved',
|
|
firesBeforeUnload() === true);
|
|
|
|
document.title = 'DONE';
|
|
});
|
|
}, 0);
|
|
});
|
|
});
|
|
});
|
|
}, 100);
|
|
}, 300);
|
|
</script>
|
|
</body>
|
|
</html>
|