save-as-ebook/tests/dom/css-editor.html
2026-08-28 21:33:22 +03:00

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>