mirror of
https://github.com/alexadam/save-as-ebook.git
synced 2026-10-21 05:06:02 +00:00
826 lines
38 KiB
HTML
826 lines
38 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>
|
|
// The style library page, in the browser. What is checked here is the part that
|
|
// needs a DOM: which rows the list shows for a search or a filter chip, what the
|
|
// detail pane says about the style that was clicked, and what gets written when a
|
|
// checkbox, a save or a delete is used. The matcher and the library rules
|
|
// themselves are node tests - see tests/style-library.js.
|
|
|
|
// The page reads the url the popup was opened on out of its own query string, so
|
|
// the fixture has to have one before cssEditor.js runs.
|
|
var TARGET_URL = 'https://example.com/articles/42';
|
|
history.replaceState(null, '', '?for=' + encodeURIComponent(TARGET_URL));
|
|
|
|
var FIXTURE_CATALOG = {
|
|
version: 1,
|
|
entries: [
|
|
{builtinId: 'wikipedia-article', title: 'Wikipedia Article', description: 'the article only',
|
|
scope: 'site', match: {type: 'regex', pattern: 'wikipedia\\.org\\/wiki\\/'},
|
|
css: '#mw-panel {display: none}', enabled: true, v1: true},
|
|
{builtinId: 'theme-serif-reading', title: 'Serif Reading', description: 'a serif face',
|
|
scope: 'theme', match: {type: 'domain', pattern: ''},
|
|
css: 'body {font-family: serif}', enabled: false}
|
|
]
|
|
};
|
|
|
|
// Built as the background would hand it over: normalized, and in the order the
|
|
// cascade is worked out from.
|
|
function fixtureLibrary() {
|
|
return normalizeStyleLibrary({version: 2, entries: [
|
|
{id: 'builtin-wikipedia-article', title: 'Wikipedia Article', origin: 'builtin',
|
|
builtinId: 'wikipedia-article', scope: 'site',
|
|
match: {type: 'regex', pattern: 'wikipedia\\.org\\/wiki\\/'},
|
|
css: '#mw-panel {display: none}'},
|
|
{id: 'ex', title: 'Example Site', origin: 'user', scope: 'site',
|
|
match: {type: 'domain', pattern: 'example.com'}, css: '.ad {display: none}'},
|
|
{id: 'off', title: 'Switched Off', origin: 'user', scope: 'site', enabled: false,
|
|
match: {type: 'domain', pattern: 'example.com'}, css: '.x {}'},
|
|
{id: 'serif', title: 'Serif Reading', origin: 'builtin',
|
|
builtinId: 'theme-serif-reading', scope: 'theme', enabled: false,
|
|
css: 'body {font-family: serif}'},
|
|
{id: 'other', title: 'Elsewhere', origin: 'user', scope: 'site',
|
|
match: {type: 'domain', pattern: 'other.test'}, css: '.y {}'}
|
|
]});
|
|
}
|
|
|
|
// A page captured by "Style this page ...", as the background stores it. The
|
|
// markup carries the page's own class names and ids, which is the whole point of
|
|
// the snapshot mode in extractHtml.js: it is what the styles being previewed
|
|
// select on, and what the element picker names things by.
|
|
function fixtureSnapshot() {
|
|
return {
|
|
url: TARGET_URL,
|
|
title: 'Example Article',
|
|
capturedAt: 1700000000000,
|
|
page: {
|
|
title: 'Example Article',
|
|
url: 'example42.xhtml',
|
|
styleFileName: 'style1.css',
|
|
styleFileContent: '.sae-c-aB0{color:rgb(1, 2, 3)}',
|
|
images: [],
|
|
content: '<div>' +
|
|
'<article class="post sae-c-aB0">' +
|
|
'<h1 id="headline">Example Article</h1>' +
|
|
'<p class="body-text">One.</p>' +
|
|
'<p class="body-text">Two.</p>' +
|
|
'</article>' +
|
|
// three kinds of class name on one element: the page's own, one a
|
|
// build tool minted, and one extraction added
|
|
'<aside class="sidebar widget css-1x9fk2 sae-c-aB0">side</aside>' +
|
|
'<div id="comments"><p>a comment</p></div>' +
|
|
'</div>'
|
|
}
|
|
};
|
|
}
|
|
|
|
var snapshot = fixtureSnapshot();
|
|
|
|
var sent = [];
|
|
var saved = [];
|
|
var alerts = [];
|
|
var confirms = [];
|
|
var confirmAnswer = true;
|
|
|
|
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 === 'get style library') {
|
|
reply = {library: fixtureLibrary(), catalog: FIXTURE_CATALOG};
|
|
}
|
|
if (msg.type === 'set style library') {
|
|
saved.push(msg.library);
|
|
reply = {ok: true};
|
|
}
|
|
if (msg.type === 'get style snapshot') {
|
|
reply = {snapshot: snapshot};
|
|
}
|
|
if (msg.type === 'clear style snapshot') {
|
|
snapshot = null;
|
|
reply = {ok: true};
|
|
}
|
|
if (cb) setTimeout(function(){ cb(reply); }, 0);
|
|
}
|
|
},
|
|
// The real messages are sentences; the key plus its substitutions is enough to
|
|
// assert on, and shows a message used with the wrong number of them.
|
|
i18n: {getMessage: function(k, subs){ return subs ? k + ':' + subs.join(',') : k; }},
|
|
tabs: {query: function(q, cb){ cb([{id: 1, url: TARGET_URL}]); },
|
|
create: function(){}, update: function(){}},
|
|
commands: {getAll: function(cb){ cb([]); }}
|
|
};
|
|
|
|
window.alert = function(message){ alerts.push(message); };
|
|
window.confirm = function(message){ confirms.push(message); return confirmAnswer; };
|
|
|
|
// Export ends in a download, which headless Chrome would actually try to
|
|
// perform. What is being tested is what gets written, so the blob is kept and
|
|
// read instead - downloadBlob is a global from utils.js, and cssEditor.js
|
|
// resolves it when it calls it.
|
|
var downloads = [];
|
|
window.addEventListener('load', function () {
|
|
window.downloadBlob = function (blob, fileName) {
|
|
downloads.push({name: fileName, blob: blob});
|
|
};
|
|
});
|
|
|
|
// The text of the two files exported below, read once they exist - a blob is
|
|
// read asynchronously, so the assertions on them run with the preview checks.
|
|
var exported = null;
|
|
// how many styles were in the library when it was exported - the filter chips
|
|
// are all off by then, so the row count is the library
|
|
var rowsAtExport = 0;
|
|
|
|
var errors = [];
|
|
window.addEventListener('error', function(e){ errors.push(e.message); });
|
|
</script>
|
|
|
|
<!-- the same list styles.html loads, in the same order -->
|
|
<script src="../../web-extension/utils.js"></script>
|
|
<script src="../../web-extension/cssSanitizer.js"></script>
|
|
<script src="../../web-extension/styleLibrary.js"></script>
|
|
<script src="../../web-extension/sanitizeHtml.js"></script>
|
|
<script src="../../web-extension/saveEbook.js"></script>
|
|
<script src="../../web-extension/chapterPreview.js"></script>
|
|
<script src="../../web-extension/stylePicker.js"></script>
|
|
<script src="../../web-extension/cssEditor.js"></script>
|
|
|
|
<script>
|
|
function report(name, ok, detail) {
|
|
var li = document.createElement('li');
|
|
li.textContent = (ok ? 'PASS ' : 'FAIL ') + name + (detail ? ' -- ' + detail : '');
|
|
document.getElementById('results').appendChild(li);
|
|
}
|
|
|
|
function rows() {
|
|
return Array.prototype.slice.call(
|
|
document.getElementsByClassName('cssEditor-style-row'));
|
|
}
|
|
|
|
function rowNames() {
|
|
return rows().map(function (row) {
|
|
return row.getElementsByClassName('cssEditor-style-row-name')[0].textContent;
|
|
});
|
|
}
|
|
|
|
function click(id) {
|
|
document.getElementById(id).click();
|
|
}
|
|
|
|
function lastSaved() {
|
|
return saved[saved.length - 1];
|
|
}
|
|
|
|
function savedEntry(id) {
|
|
var library = lastSaved();
|
|
if (!library) {
|
|
return null;
|
|
}
|
|
return library.entries.filter(function (entry) { return entry.id === id; })[0] || null;
|
|
}
|
|
|
|
setTimeout(function () {
|
|
var out = document.createElement('ul');
|
|
out.id = 'results';
|
|
document.body.appendChild(out);
|
|
|
|
report('library page script ran without throwing', errors.length === 0, errors.join(' | '));
|
|
report('page rendered', !!document.getElementById('cssEditor-Modal') &&
|
|
!!document.getElementById('cssEditor-styleList'));
|
|
report('the library was asked for', sent.indexOf('get style library') > -1);
|
|
|
|
// ---- the list ----
|
|
report('every style is a row, in the library\'s own order',
|
|
rowNames().join('|') === 'Wikipedia Article|Example Site|Switched Off|Serif Reading|Elsewhere',
|
|
rowNames().join('|'));
|
|
|
|
var firstRow = rows()[0];
|
|
report('a row says what the style matches, in words rather than as a regex',
|
|
firstRow.getElementsByClassName('cssEditor-style-row-where')[0].textContent ===
|
|
'styleMatchDescRegex:wikipedia\\.org\\/wiki\\/',
|
|
firstRow.getElementsByClassName('cssEditor-style-row-where')[0].textContent);
|
|
report('a row that applies to every capture says so',
|
|
rows()[3].getElementsByClassName('cssEditor-style-row-where')[0].textContent ===
|
|
'styleAppliesEveryPage');
|
|
report('a row is badged with where the style came from',
|
|
firstRow.getElementsByClassName('cssEditor-badge')[0].textContent === 'styleOriginBuiltin' &&
|
|
rows()[1].getElementsByClassName('cssEditor-badge')[0].textContent === 'styleOriginUser');
|
|
report('a switched-off style has an unchecked box, not a missing row',
|
|
rows()[2].getElementsByClassName('cssEditor-style-toggle')[0].checked === false &&
|
|
rows()[1].getElementsByClassName('cssEditor-style-toggle')[0].checked === true);
|
|
|
|
// ---- search and the filter chips ----
|
|
var search = document.getElementById('cssEditor-search');
|
|
search.value = 'wikipedia';
|
|
search.dispatchEvent(new Event('input'));
|
|
report('search covers the name and the pattern', rowNames().join('|') === 'Wikipedia Article',
|
|
rowNames().join('|'));
|
|
|
|
search.value = 'font-family';
|
|
search.dispatchEvent(new Event('input'));
|
|
report('search covers the CSS body', rowNames().join('|') === 'Serif Reading',
|
|
rowNames().join('|'));
|
|
|
|
search.value = 'no such style';
|
|
search.dispatchEvent(new Event('input'));
|
|
report('a search that finds nothing says so', rows().length === 0 &&
|
|
document.getElementById('cssEditor-listEmpty').textContent === 'styleListEmpty');
|
|
|
|
search.value = '';
|
|
search.dispatchEvent(new Event('input'));
|
|
|
|
click('cssEditor-filter-theme');
|
|
report('the every-page chip shows only the styles that apply to every capture',
|
|
rowNames().join('|') === 'Serif Reading', rowNames().join('|'));
|
|
|
|
click('cssEditor-filter-site');
|
|
report('the site chip shows only pattern-matched styles', rows().length === 4,
|
|
rowNames().join('|'));
|
|
|
|
click('cssEditor-filter-enabled');
|
|
report('the on chip shows only what is switched on',
|
|
rowNames().join('|') === 'Wikipedia Article|Example Site|Elsewhere', rowNames().join('|'));
|
|
|
|
click('cssEditor-filter-builtin');
|
|
report('the built-in chip shows the bundled styles nobody has edited',
|
|
rowNames().join('|') === 'Wikipedia Article|Serif Reading', rowNames().join('|'));
|
|
|
|
click('cssEditor-filter-mine');
|
|
report('the mine chip shows the rest',
|
|
rowNames().join('|') === 'Example Site|Switched Off|Elsewhere', rowNames().join('|'));
|
|
|
|
// The question the old regex box could not answer: what runs on this page.
|
|
click('cssEditor-filter-matches');
|
|
report('the applies-here chip is about the page the popup was opened on',
|
|
rowNames().join('|') === 'Example Site|Switched Off|Serif Reading', rowNames().join('|'));
|
|
|
|
click('cssEditor-filter-all');
|
|
|
|
// ---- the detail pane ----
|
|
report('nothing is selected to begin with',
|
|
document.getElementById('cssEditor-noSelection').style.display === 'block' &&
|
|
document.getElementById('cssEditor-styleEditor').style.display === 'none' &&
|
|
document.getElementById('cssEditor-saveStyle').style.display === 'none');
|
|
|
|
click('cssEditor-row-ex');
|
|
report('clicking a row opens that style',
|
|
document.getElementById('cssEditor-styleEditor').style.display === 'flex' &&
|
|
document.getElementById('cssEditor-styleName').value === 'Example Site' &&
|
|
document.getElementById('cssEditor-matchType').value === 'domain' &&
|
|
document.getElementById('cssEditor-matchUrl').value === 'example.com' &&
|
|
document.getElementById('cssEditor-styleContent').value === '.ad {display: none}');
|
|
report('the selected row is marked as selected',
|
|
document.getElementById('cssEditor-row-ex').className.indexOf('cssEditor-style-row-on') > -1);
|
|
|
|
report('the test url is prefilled with the page the popup was opened on',
|
|
document.getElementById('cssEditor-testUrl').value === TARGET_URL);
|
|
report('the pattern is reported as matching that url',
|
|
document.getElementById('cssEditor-matchResult').textContent === 'styleTestMatches' &&
|
|
document.getElementById('cssEditor-matchResult').className === 'cssEditor-match-yes',
|
|
document.getElementById('cssEditor-matchResult').textContent);
|
|
|
|
var pattern = document.getElementById('cssEditor-matchUrl');
|
|
pattern.value = 'other.test';
|
|
pattern.dispatchEvent(new Event('input'));
|
|
report('a pattern that does not cover the url says that, and not that it is wrong',
|
|
document.getElementById('cssEditor-matchResult').textContent === 'styleTestNoMatch');
|
|
|
|
// A half-typed regex matches nothing, so "does not match" would be a fact about
|
|
// the pattern dressed up as a fact about the url.
|
|
document.getElementById('cssEditor-matchType').value = 'regex';
|
|
document.getElementById('cssEditor-matchType').dispatchEvent(new Event('change'));
|
|
pattern.value = 'example\\.com\\/[';
|
|
pattern.dispatchEvent(new Event('input'));
|
|
report('a pattern that cannot compile is called out as such',
|
|
document.getElementById('cssEditor-matchResult').textContent === 'styleTestInvalid' &&
|
|
document.getElementById('cssEditor-matchResult').className === 'cssEditor-match-bad');
|
|
|
|
click('cssEditor-saveStyle');
|
|
report('a style with an unusable regex is not saved', saved.length === 0 &&
|
|
alerts.join('|') === 'invalidRegex', alerts.join('|'));
|
|
|
|
// A pattern that compiles but backtracks exponentially is the worse of the
|
|
// two: matching runs in the service worker on every capture, with nothing
|
|
// able to interrupt it. It is refused where it is written.
|
|
alerts = [];
|
|
pattern.value = '(a+)+example\\.com';
|
|
pattern.dispatchEvent(new Event('input'));
|
|
report('a pattern that would hang the worker is called out while it is typed',
|
|
document.getElementById('cssEditor-matchResult').textContent
|
|
.indexOf('styleRegexUnsafe') === 0 &&
|
|
document.getElementById('cssEditor-matchResult').className === 'cssEditor-match-bad',
|
|
document.getElementById('cssEditor-matchResult').textContent);
|
|
|
|
click('cssEditor-saveStyle');
|
|
report('and is not saved either', saved.length === 0 &&
|
|
alerts.join('|').indexOf('styleRegexUnsafe') === 0, alerts.join('|'));
|
|
|
|
// A style that applies to every capture has no pattern to show.
|
|
var scope = document.getElementById('cssEditor-styleScope');
|
|
scope.value = 'theme';
|
|
scope.dispatchEvent(new Event('change'));
|
|
report('the match fields go away for an every-page style',
|
|
document.getElementById('cssEditor-matchHolder').style.display === 'none');
|
|
scope.value = 'site';
|
|
scope.dispatchEvent(new Event('change'));
|
|
report('and come back', document.getElementById('cssEditor-matchHolder').style.display === 'block');
|
|
|
|
// ---- writing ----
|
|
alerts = [];
|
|
document.getElementById('cssEditor-matchType').value = 'domain';
|
|
document.getElementById('cssEditor-matchType').dispatchEvent(new Event('change'));
|
|
pattern.value = '';
|
|
pattern.dispatchEvent(new Event('input'));
|
|
click('cssEditor-saveStyle');
|
|
report('a site style with no pattern is not saved either',
|
|
saved.length === 0 && alerts.join('|') === 'styleNeedsPattern', alerts.join('|'));
|
|
|
|
alerts = [];
|
|
pattern.value = 'example.com';
|
|
pattern.dispatchEvent(new Event('input'));
|
|
document.getElementById('cssEditor-styleContent').value = '.ad, .promo {display: none}';
|
|
click('cssEditor-saveStyle');
|
|
report('saving writes the whole library, with the edit in it',
|
|
saved.length === 1 && savedEntry('ex').css === '.ad, .promo {display: none}' &&
|
|
lastSaved().entries.length === 5 && alerts.join('|') === 'styleSaved',
|
|
alerts.join('|'));
|
|
|
|
// The checkbox is not part of the draft: it is answered where it is clicked.
|
|
rows()[1].getElementsByClassName('cssEditor-style-toggle')[0].click();
|
|
report('a checkbox writes straight away',
|
|
saved.length === 2 && savedEntry('ex').enabled === false);
|
|
report('and the row it was clicked on shows it',
|
|
rows()[1].getElementsByClassName('cssEditor-style-toggle')[0].checked === false);
|
|
|
|
// ---- built-ins ----
|
|
click('cssEditor-row-builtin-wikipedia-article');
|
|
report('a bundled style says it is one, and offers nothing to reset to',
|
|
document.getElementById('cssEditor-originBadge').textContent === 'styleOriginBuiltin' &&
|
|
document.getElementById('cssEditor-resetStyle').style.display === 'none');
|
|
|
|
document.getElementById('cssEditor-styleContent').value = '#mw-panel {display: none} p {color: red}';
|
|
click('cssEditor-saveStyle');
|
|
report('editing a bundled style makes it the user\'s copy of it',
|
|
savedEntry('builtin-wikipedia-article').origin === 'user' &&
|
|
savedEntry('builtin-wikipedia-article').builtinId === 'wikipedia-article');
|
|
report('and it can now be reset',
|
|
document.getElementById('cssEditor-resetStyle').style.display === 'inline-block' &&
|
|
document.getElementById('cssEditor-originBadge').textContent === 'styleOriginForked');
|
|
|
|
click('cssEditor-resetStyle');
|
|
report('resetting asks first, then puts the bundled style back',
|
|
confirms.join('|') === 'styleResetConfirm' &&
|
|
savedEntry('builtin-wikipedia-article').css === '#mw-panel {display: none}' &&
|
|
savedEntry('builtin-wikipedia-article').origin === 'builtin',
|
|
confirms.join('|'));
|
|
|
|
// ---- duplicate, new, delete ----
|
|
confirms = [];
|
|
var before = rows().length;
|
|
click('cssEditor-duplicateStyle');
|
|
report('a duplicate sits next to the style it came from, as the user\'s own',
|
|
rows().length === before + 1 &&
|
|
rowNames()[1] === 'styleCopyTitle:Wikipedia Article' &&
|
|
lastSaved().entries[1].builtinId === null &&
|
|
lastSaved().entries[1].origin === 'user',
|
|
rowNames().join('|'));
|
|
report('the copy is what is now being edited',
|
|
document.getElementById('cssEditor-styleName').value === 'styleCopyTitle:Wikipedia Article');
|
|
|
|
click('cssEditor-newSiteStyle');
|
|
report('a new site style is prefilled with the domain of that page',
|
|
document.getElementById('cssEditor-matchType').value === 'domain' &&
|
|
document.getElementById('cssEditor-matchUrl').value === 'example.com' &&
|
|
document.getElementById('cssEditor-styleName').value === 'newStyleTitle',
|
|
document.getElementById('cssEditor-matchUrl').value);
|
|
report('and it is in the library, not a draft beside it',
|
|
rowNames().indexOf('newStyleTitle') > -1);
|
|
|
|
click('cssEditor-newThemeStyle');
|
|
report('a new every-page style has no pattern fields',
|
|
document.getElementById('cssEditor-styleScope').value === 'theme' &&
|
|
document.getElementById('cssEditor-matchHolder').style.display === 'none');
|
|
|
|
confirms = [];
|
|
var rowsBeforeDelete = rows().length;
|
|
click('cssEditor-removeStyle');
|
|
report('deleting asks first, and removes what was selected',
|
|
confirms.join('|') === 'confirmDeleteStyle' && rows().length === rowsBeforeDelete - 1 &&
|
|
document.getElementById('cssEditor-styleEditor').style.display === 'none',
|
|
confirms.join('|'));
|
|
|
|
confirmAnswer = false;
|
|
confirms = [];
|
|
click('cssEditor-row-other');
|
|
var keptRows = rows().length;
|
|
click('cssEditor-removeStyle');
|
|
report('a delete that is not confirmed leaves the style alone',
|
|
rows().length === keptRows && !!document.getElementById('cssEditor-row-other'));
|
|
|
|
// An unsaved edit is the one thing on this page that can be lost, so it is the
|
|
// one thing that asks.
|
|
document.getElementById('cssEditor-styleContent').value = '.unsaved {}';
|
|
document.getElementById('cssEditor-styleContent').dispatchEvent(new Event('input'));
|
|
confirms = [];
|
|
click('cssEditor-row-ex');
|
|
report('selecting another style with unsaved changes asks, and stays put when refused',
|
|
confirms.join('|') === 'styleDiscardEditsConfirm' &&
|
|
document.getElementById('cssEditor-styleContent').value === '.unsaved {}',
|
|
confirms.join('|'));
|
|
|
|
confirmAnswer = true;
|
|
click('cssEditor-row-ex');
|
|
report('and moves on when allowed',
|
|
document.getElementById('cssEditor-styleName').value === 'Example Site');
|
|
|
|
// Deleting a bundled style has to be remembered, or the catalog hands it back.
|
|
click('cssEditor-row-serif');
|
|
click('cssEditor-removeStyle');
|
|
report('a deleted bundled style is remembered as deleted',
|
|
lastSaved().removedBuiltins.indexOf('theme-serif-reading') > -1,
|
|
JSON.stringify(lastSaved().removedBuiltins));
|
|
|
|
// ---- export ----
|
|
//
|
|
// What is written is covered by the node tests; what needs a page is that the
|
|
// buttons write the style the user is looking at rather than the stored one,
|
|
// and that the whole library goes out under one name.
|
|
confirmAnswer = true;
|
|
click('cssEditor-row-ex');
|
|
var name = document.getElementById('cssEditor-styleName');
|
|
name.value = 'Example Site (edited)';
|
|
name.dispatchEvent(new Event('input'));
|
|
|
|
click('cssEditor-exportStyle');
|
|
report('exporting one style writes a file named after it',
|
|
downloads.length === 1 &&
|
|
downloads[0].name === 'save-as-ebook-style-example-site-edited.json',
|
|
downloads.length ? downloads[0].name : 'nothing was downloaded');
|
|
|
|
rowsAtExport = rows().length;
|
|
click('cssEditor-exportAllStyles');
|
|
report('exporting the library writes one file holding all of it',
|
|
downloads.length === 2 && downloads[1].name === 'save-as-ebook-styles.json',
|
|
downloads.length > 1 ? downloads[1].name : 'nothing was downloaded');
|
|
|
|
// read once here, asserted after the frame checks - a blob is read
|
|
// asynchronously and there is nothing to wait on in the middle of this block
|
|
Promise.all(downloads.map(function (download) { return download.blob.text(); }))
|
|
.then(function (texts) { exported = texts; });
|
|
|
|
// the title above was never saved, and the import below would ask about it
|
|
click('cssEditor-row-other');
|
|
click('cssEditor-row-ex');
|
|
|
|
// ---- import ----
|
|
var IMPORTED_FILE = JSON.stringify({
|
|
kind: 'save-as-ebook-styles', version: 2, entries: [
|
|
// the same id as the style already in the library: a newer copy of one
|
|
// that was shared from here
|
|
{id: 'ex', title: 'Example Site (shared)', scope: 'site',
|
|
match: {type: 'domain', pattern: 'example.com'},
|
|
css: '.shared {display: none}\n' +
|
|
'.beacon {background: url("https://tracker.test/pixel.png");}'},
|
|
{id: 'brand-new', title: 'Brand New', scope: 'site',
|
|
match: {type: 'domain', pattern: 'new.test'}, css: '.n {display: none}'}
|
|
]
|
|
});
|
|
|
|
function importSummaryText() {
|
|
return oneLine(document.getElementById('cssEditor-importSummary').textContent);
|
|
}
|
|
|
|
// Reading a file is not importing it: nothing below writes until one of the
|
|
// two buttons at the bottom of the panel is used.
|
|
var savedBeforeImport = saved.length;
|
|
|
|
click('cssEditor-importStyles');
|
|
report('the import panel opens asking for a file or some text',
|
|
document.getElementById('cssEditor-importOverlay').style.display === 'flex' &&
|
|
document.getElementById('cssEditor-importPickStep').style.display === 'block' &&
|
|
document.getElementById('cssEditor-importSummary').style.display === 'none' &&
|
|
document.getElementById('cssEditor-importApply').style.display === 'none');
|
|
|
|
click('cssEditor-importRead');
|
|
report('an empty box is not an import',
|
|
document.getElementById('cssEditor-importError').textContent ===
|
|
'styleImportNothingPasted' && saved.length === savedBeforeImport,
|
|
document.getElementById('cssEditor-importError').textContent);
|
|
|
|
document.getElementById('cssEditor-importText').value = 'this is not a style file';
|
|
click('cssEditor-importRead');
|
|
report('text that is not a style file is refused, and nothing is written',
|
|
document.getElementById('cssEditor-importError').textContent ===
|
|
'styleImportNotAStyleFile' &&
|
|
document.getElementById('cssEditor-importSummary').style.display === 'none' &&
|
|
saved.length === savedBeforeImport,
|
|
document.getElementById('cssEditor-importError').textContent);
|
|
|
|
// A style file is a file somebody else wrote, and the matcher will not run a
|
|
// pattern like this one. The style can still be imported - the pattern is the
|
|
// only unusable part of it - so the panel says so rather than the user
|
|
// wondering later why the css never appears.
|
|
document.getElementById('cssEditor-importText').value = JSON.stringify([
|
|
{id: 'slow', title: 'Slow One', scope: 'site', css: '.s {display: none}',
|
|
match: {type: 'regex', pattern: '(a+)+b'}}
|
|
]);
|
|
click('cssEditor-importRead');
|
|
report('a pattern the matcher will not run is named before the import is applied',
|
|
importSummaryText().indexOf('styleImportUnusablePatterns') > -1 &&
|
|
importSummaryText().indexOf('Slow One: (a+)+b') > -1 &&
|
|
saved.length === savedBeforeImport,
|
|
importSummaryText());
|
|
|
|
document.getElementById('cssEditor-importText').value = IMPORTED_FILE;
|
|
click('cssEditor-importRead');
|
|
report('the panel says what would be added before anything is written',
|
|
importSummaryText().indexOf('styleImportAdding:1') > -1 &&
|
|
importSummaryText().indexOf('Brand New') > -1 &&
|
|
saved.length === savedBeforeImport,
|
|
importSummaryText());
|
|
report('and shows both sides of a style the library already has',
|
|
importSummaryText().indexOf('styleImportCollisions:1') > -1 &&
|
|
importSummaryText().indexOf('styleImportHere:Example Site,') > -1 &&
|
|
importSummaryText().indexOf('styleImportDifferentCss') > -1,
|
|
importSummaryText());
|
|
// Not a footnote: this is css the file asked to run on the user's own pages.
|
|
report('and what was taken out of the imported css, with the reason',
|
|
importSummaryText().indexOf('styleImportStripped') > -1 &&
|
|
importSummaryText().indexOf('https://tracker.test/pixel.png') > -1,
|
|
importSummaryText());
|
|
report('replacing is offered only because there is something to replace',
|
|
document.getElementById('cssEditor-importReplace').style.display === 'inline-block' &&
|
|
document.getElementById('cssEditor-importApply').textContent === 'styleImportKeepBoth');
|
|
|
|
alerts = [];
|
|
click('cssEditor-importApply');
|
|
report('keeping both adds the imported style beside the one already here',
|
|
savedEntry('ex').title === 'Example Site' &&
|
|
lastSaved().entries.filter(function (entry) {
|
|
return entry.title === 'Example Site (shared)';
|
|
}).length === 1 &&
|
|
alerts.join('|') === 'styleImported:2',
|
|
alerts.join('|'));
|
|
report('the imported css lost the address it would have fetched from',
|
|
lastSaved().entries.filter(function (entry) {
|
|
return entry.css.indexOf('tracker.test') > -1;
|
|
}).length === 0 &&
|
|
lastSaved().entries.filter(function (entry) {
|
|
return entry.css.indexOf('.shared {display: none}') > -1;
|
|
}).length === 1);
|
|
report('the panel closes and what arrived is what is being edited',
|
|
document.getElementById('cssEditor-importOverlay').style.display === 'none' &&
|
|
document.getElementById('cssEditor-styleName').value === 'Example Site (shared)',
|
|
document.getElementById('cssEditor-styleName').value);
|
|
report('an imported style is the user\'s own, whatever the file said',
|
|
savedEntry('brand-new').origin === 'imported' &&
|
|
savedEntry('brand-new').builtinId === null);
|
|
|
|
// The other answer to the same question.
|
|
click('cssEditor-importStyles');
|
|
document.getElementById('cssEditor-importText').value = IMPORTED_FILE;
|
|
click('cssEditor-importRead');
|
|
click('cssEditor-importReplace');
|
|
report('replacing puts the imported style in the place of the one it names',
|
|
savedEntry('ex').title === 'Example Site (shared)' &&
|
|
savedEntry('ex').css.indexOf('.shared') > -1,
|
|
savedEntry('ex') ? savedEntry('ex').title : 'gone');
|
|
|
|
click('cssEditor-importStyles');
|
|
document.onkeydown({keyCode: 27});
|
|
report('escape closes the import panel rather than the page',
|
|
document.getElementById('cssEditor-importOverlay').style.display === 'none' &&
|
|
!!document.getElementById('cssEditor-styleList'));
|
|
|
|
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);
|
|
|
|
// The preview frame loads its srcdoc asynchronously, and the picker only
|
|
// exists once it has - so the rest runs after it.
|
|
setTimeout(previewChecks, 400);
|
|
}, 300);
|
|
|
|
// run-dom-tests.sh reads the results a line at a time, so a detail carrying a
|
|
// newline loses the assertion it belongs to - and css is full of them.
|
|
function oneLine(text) {
|
|
return String(text).replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
function previewFrameDocument() {
|
|
var frame = document.getElementById('cssEditor-previewFrame');
|
|
return frame ? frame.contentDocument : null;
|
|
}
|
|
|
|
function frameComputed(selector, property) {
|
|
var doc = previewFrameDocument();
|
|
var elem = doc ? doc.querySelector(selector) : null;
|
|
return elem ? doc.defaultView.getComputedStyle(elem)[property] : '';
|
|
}
|
|
|
|
function frameStyleText(id) {
|
|
var doc = previewFrameDocument();
|
|
var elem = doc ? doc.getElementById(id) : null;
|
|
return elem ? elem.textContent : '';
|
|
}
|
|
|
|
// A pointer moving onto an element in the frame and clicking it. The picker
|
|
// listens on the frame document with capture, so an event raised on the element
|
|
// and allowed to bubble is the same thing a real pointer produces.
|
|
function pointAt(element) {
|
|
element.dispatchEvent(new MouseEvent('mousemove', {bubbles: true}));
|
|
}
|
|
function clickIn(element) {
|
|
element.dispatchEvent(new MouseEvent('click', {bubbles: true, cancelable: true}));
|
|
}
|
|
|
|
function previewChecks() {
|
|
var doc = previewFrameDocument();
|
|
|
|
report('the captured page was asked for', sent.indexOf('get style snapshot') > -1);
|
|
report('the preview says which page it is showing',
|
|
document.getElementById('cssEditor-previewSource').textContent ===
|
|
'stylePreviewOf:' + TARGET_URL,
|
|
document.getElementById('cssEditor-previewSource').textContent);
|
|
report('the captured page is rendered in a frame', !!doc && !!doc.body);
|
|
|
|
// The contract snapshot mode exists for: a style is written against the page's
|
|
// own names, so those are what the frame has to hold. A normal capture would
|
|
// have replaced them with the generated ones.
|
|
report('the page\'s own class names and ids survived into the snapshot',
|
|
!!doc && doc.querySelectorAll('aside.sidebar').length === 1 &&
|
|
doc.querySelectorAll('#comments').length === 1 &&
|
|
doc.querySelectorAll('p.body-text').length === 2,
|
|
doc ? doc.body.innerHTML.substring(0, 120) : 'no frame');
|
|
|
|
// ---- the styles being previewed ----
|
|
click('cssEditor-row-ex');
|
|
var css = document.getElementById('cssEditor-styleContent');
|
|
css.value = 'aside.sidebar {display: none}';
|
|
css.dispatchEvent(new Event('input'));
|
|
report('typing a rule takes effect in the frame as it is typed',
|
|
frameComputed('aside.sidebar', 'display') === 'none',
|
|
frameComputed('aside.sidebar', 'display'));
|
|
report('and it went into the site style sheet, not the chapter\'s',
|
|
frameStyleText('sae-preview-site-css').indexOf('aside.sidebar') > -1 &&
|
|
frameStyleText('sae-preview-chapter-css').indexOf('aside.sidebar') < 0);
|
|
report('the pane names the styles it is showing',
|
|
document.getElementById('cssEditor-previewApplied').textContent
|
|
.indexOf('Example Site') > -1,
|
|
document.getElementById('cssEditor-previewApplied').textContent);
|
|
|
|
// The list is rebuilt by every write, so the row is looked up again each time.
|
|
// The assertions above left this style switched off.
|
|
function exToggle() {
|
|
return document.getElementById('cssEditor-row-ex')
|
|
.getElementsByClassName('cssEditor-style-toggle')[0];
|
|
}
|
|
function setEx(on) {
|
|
if (exToggle().checked !== on) {
|
|
exToggle().click();
|
|
}
|
|
}
|
|
|
|
// A checkbox writes to storage as it is clicked. The box beside it is a draft,
|
|
// and is not what was being answered.
|
|
exToggle().click();
|
|
exToggle().click();
|
|
report('a checkbox does not throw away what is being typed',
|
|
css.value === 'aside.sidebar {display: none}', oneLine(css.value));
|
|
|
|
// Stored and then left, so that what follows is about a style being switched
|
|
// off rather than about a draft: the style being edited is previewed whether
|
|
// it is switched on or not, because it is the one being looked at.
|
|
click('cssEditor-saveStyle');
|
|
click('cssEditor-row-other');
|
|
|
|
setEx(true);
|
|
report('a style that is switched on is applied to the preview',
|
|
frameComputed('aside.sidebar', 'display') === 'none',
|
|
frameComputed('aside.sidebar', 'display'));
|
|
setEx(false);
|
|
report('and switching it off takes it out again',
|
|
frameComputed('aside.sidebar', 'display') !== 'none',
|
|
frameComputed('aside.sidebar', 'display'));
|
|
|
|
// ---- recipes ----
|
|
css.value = '';
|
|
css.dispatchEvent(new Event('input'));
|
|
click('cssEditor-recipe-hideImages');
|
|
report('a recipe writes a rule into the box',
|
|
css.value.indexOf('img') > -1 && css.value.indexOf('display: none') > -1,
|
|
oneLine(css.value));
|
|
click('cssEditor-recipe-biggerText');
|
|
report('and a second one is added to the first, not put in its place',
|
|
css.value.indexOf('img') > -1 && css.value.indexOf('font-size') > -1);
|
|
|
|
// ---- the element picker ----
|
|
css.value = '';
|
|
css.dispatchEvent(new Event('input'));
|
|
click('cssEditor-pickElement');
|
|
report('picking is off until it is turned on, and then on',
|
|
document.getElementById('cssEditor-pickElement').className
|
|
.indexOf('cssEditor-chip-on') > -1);
|
|
|
|
var aside = doc.querySelector('aside.sidebar');
|
|
pointAt(aside);
|
|
report('what a click would hide is shown before it is clicked',
|
|
document.getElementById('cssEditor-pickTarget').textContent ===
|
|
'stylePickTarget:aside.sidebar',
|
|
document.getElementById('cssEditor-pickTarget').textContent);
|
|
report('and the element under the pointer is highlighted',
|
|
aside.className.indexOf('sae-pick-hover') > -1, aside.className);
|
|
|
|
clickIn(aside);
|
|
// The selector has to be about the site, not about this snapshot of it: the
|
|
// class extraction added is minted afresh by every capture, and the one the
|
|
// site's build tool minted is gone at its next deploy.
|
|
report('clicking writes a rule naming the page\'s own class, not a generated one',
|
|
css.value.indexOf('aside.sidebar {') > -1 &&
|
|
css.value.indexOf('display: none !important') > -1 &&
|
|
css.value.indexOf('sae-c-') < 0 && css.value.indexOf('css-1x9fk2') < 0,
|
|
oneLine(css.value));
|
|
report('and the preview shows what it did',
|
|
frameComputed('aside.sidebar', 'display') === 'none');
|
|
|
|
// An element with a usable id is named by it - the shortest thing that still
|
|
// means the same after the site is rebuilt.
|
|
var comments = doc.querySelector('#comments');
|
|
pointAt(comments);
|
|
report('an element with an id is named by it',
|
|
document.getElementById('cssEditor-pickTarget').textContent ===
|
|
'stylePickTarget:#comments',
|
|
document.getElementById('cssEditor-pickTarget').textContent);
|
|
|
|
// One of two identical paragraphs: no selector names it alone, so the one
|
|
// written covers both, and that is said rather than hidden.
|
|
pointAt(doc.querySelectorAll('p.body-text')[0]);
|
|
report('a selector that covers more than one element says how many',
|
|
document.getElementById('cssEditor-pickTarget').textContent ===
|
|
'stylePickTargetShared:article.post p.body-text,2',
|
|
document.getElementById('cssEditor-pickTarget').textContent);
|
|
|
|
// Widening is what gets from a paragraph to the block around it.
|
|
click('cssEditor-pickWiden');
|
|
report('select parent widens what a click would take',
|
|
document.getElementById('cssEditor-pickTarget').textContent ===
|
|
'stylePickTarget:article.post',
|
|
document.getElementById('cssEditor-pickTarget').textContent);
|
|
click('cssEditor-pickNarrow');
|
|
report('and select child narrows it again',
|
|
document.getElementById('cssEditor-pickTarget').textContent ===
|
|
'stylePickTargetShared:article.post p.body-text,2');
|
|
|
|
click('cssEditor-pickElement');
|
|
report('turning picking off drops the highlight',
|
|
doc.querySelectorAll('.sae-pick-hover').length === 0);
|
|
|
|
// ---- discarding the snapshot ----
|
|
confirmAnswer = true;
|
|
confirms = [];
|
|
click('cssEditor-previewClear');
|
|
report('discarding the captured page asks, then leaves the library usable',
|
|
confirms.join('|') === 'stylePreviewClearConfirm' &&
|
|
sent.indexOf('clear style snapshot') > -1 &&
|
|
!document.getElementById('cssEditor-previewFrame') &&
|
|
!!document.getElementById('cssEditor-previewEmpty') &&
|
|
!!document.getElementById('cssEditor-styleList'),
|
|
confirms.join('|'));
|
|
|
|
// ---- what the exports actually held ----
|
|
var one = exported ? JSON.parse(exported[0]) : null;
|
|
var all = exported ? JSON.parse(exported[1]) : null;
|
|
|
|
report('an exported style is the one on screen, not the one last stored',
|
|
!!one && one.entries.length === 1 &&
|
|
one.entries[0].title === 'Example Site (edited)',
|
|
one ? JSON.stringify(one.entries.map(function (e) { return e.title; })) : 'not read');
|
|
report('and it carries what the style is without what this install did with it',
|
|
!!one && one.entries[0].origin === undefined &&
|
|
one.entries[0].builtinId === undefined &&
|
|
one.entries[0].match.pattern === 'example.com',
|
|
one ? Object.keys(one.entries[0]).sort().join(',') : 'not read');
|
|
report('exporting the library holds every style in it',
|
|
!!all && all.kind === 'save-as-ebook-styles' &&
|
|
all.entries.length === rowsAtExport,
|
|
all ? all.entries.length + ' of ' + rowsAtExport : 'not read');
|
|
|
|
report('nothing threw in the preview half either', errors.length === 0,
|
|
errors.join(' | '));
|
|
|
|
document.title = 'DONE';
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|