diff --git a/assets/js/__tests__/imagesclientside.spec.ts b/assets/js/__tests__/imagesclientside.spec.ts
new file mode 100644
index 00000000..3f3feb88
--- /dev/null
+++ b/assets/js/__tests__/imagesclientside.spec.ts
@@ -0,0 +1,161 @@
+import { filterNode, initImagesClientside } from '../imagesclientside';
+import { parseSearch } from '../match_query';
+import { matchNone } from '../query/boolean';
+import { assertNotNull } from '../utils/assert';
+import { $ } from '../utils/dom';
+
+describe('filterNode', () => {
+ beforeEach(() => {
+ window.booru.hiddenTagList = [];
+ window.booru.spoileredTagList = [];
+ window.booru.ignoredTagList = [];
+ window.booru.imagesWithDownvotingDisabled = [];
+
+ window.booru.hiddenFilter = matchNone();
+ window.booru.spoileredFilter = matchNone();
+ });
+
+ function makeMediaContainer() {
+ const element = document.createElement('div');
+ element.innerHTML = `
+
+
+
+
+ `;
+ return [ element, assertNotNull($('.js-spoiler-info-overlay', element)) ];
+ }
+
+ it('should show image media boxes not matching any filter', () => {
+ const [ container, spoilerOverlay ] = makeMediaContainer();
+
+ filterNode(container);
+ expect(spoilerOverlay).not.toContainHTML('(Complex Filter)');
+ expect(spoilerOverlay).not.toContainHTML('(unknown tag)');
+ expect(window.booru.imagesWithDownvotingDisabled).not.toContain('1');
+ });
+
+ it('should spoiler media boxes spoilered by a tag filter', () => {
+ const [ container, spoilerOverlay ] = makeMediaContainer();
+ window.booru.spoileredTagList = [1];
+
+ filterNode(container);
+ expect(spoilerOverlay).toContainHTML('(unknown tag)');
+ expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
+ });
+
+ it('should spoiler media boxes spoilered by a complex filter', () => {
+ const [ container, spoilerOverlay ] = makeMediaContainer();
+ window.booru.spoileredFilter = parseSearch('id:1');
+
+ filterNode(container);
+ expect(spoilerOverlay).toContainHTML('(Complex Filter)');
+ expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
+ });
+
+ it('should hide media boxes hidden by a tag filter', () => {
+ const [ container, spoilerOverlay ] = makeMediaContainer();
+ window.booru.hiddenTagList = [1];
+
+ filterNode(container);
+ expect(spoilerOverlay).toContainHTML('[HIDDEN]');
+ expect(spoilerOverlay).toContainHTML('(unknown tag)');
+ expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
+ });
+
+ it('should hide media boxes hidden by a complex filter', () => {
+ const [ container, spoilerOverlay ] = makeMediaContainer();
+ window.booru.hiddenFilter = parseSearch('id:1');
+
+ filterNode(container);
+ expect(spoilerOverlay).toContainHTML('[HIDDEN]');
+ expect(spoilerOverlay).toContainHTML('(Complex Filter)');
+ expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
+ });
+
+ function makeImageBlock(): HTMLElement[] {
+ const element = document.createElement('div');
+ element.innerHTML = `
+
+
+
+
+
+
+
+
+
+ `;
+ return [
+ element,
+ assertNotNull($('.image-filtered', element)),
+ assertNotNull($('.image-show', element)),
+ assertNotNull($('.filter-explanation', element))
+ ];
+ }
+
+ it('should show image blocks not matching any filter', () => {
+ const [ container, imageFiltered, imageShow ] = makeImageBlock();
+
+ filterNode(container);
+ expect(imageFiltered).toHaveClass('hidden');
+ expect(imageShow).not.toHaveClass('hidden');
+ expect(window.booru.imagesWithDownvotingDisabled).not.toContain('1');
+ });
+
+ it('should spoiler image blocks spoilered by a tag filter', () => {
+ const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
+ window.booru.spoileredTagList = [1];
+
+ filterNode(container);
+ expect(imageFiltered).not.toHaveClass('hidden');
+ expect(imageShow).toHaveClass('hidden');
+ expect(filterExplanation).toContainHTML('spoilered by');
+ expect(filterExplanation).toContainHTML('(unknown tag)');
+ expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
+ });
+
+ it('should spoiler image blocks spoilered by a complex filter', () => {
+ const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
+ window.booru.spoileredFilter = parseSearch('id:1');
+
+ filterNode(container);
+ expect(imageFiltered).not.toHaveClass('hidden');
+ expect(imageShow).toHaveClass('hidden');
+ expect(filterExplanation).toContainHTML('spoilered by');
+ expect(filterExplanation).toContainHTML('complex tag expression');
+ expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
+ });
+
+ it('should hide image blocks hidden by a tag filter', () => {
+ const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
+ window.booru.hiddenTagList = [1];
+
+ filterNode(container);
+ expect(imageFiltered).not.toHaveClass('hidden');
+ expect(imageShow).toHaveClass('hidden');
+ expect(filterExplanation).toContainHTML('hidden by');
+ expect(filterExplanation).toContainHTML('(unknown tag)');
+ expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
+ });
+
+ it('should hide image blocks hidden by a complex filter', () => {
+ const [ container, imageFiltered, imageShow, filterExplanation ] = makeImageBlock();
+ window.booru.hiddenFilter = parseSearch('id:1');
+
+ filterNode(container);
+ expect(imageFiltered).not.toHaveClass('hidden');
+ expect(imageShow).toHaveClass('hidden');
+ expect(filterExplanation).toContainHTML('hidden by');
+ expect(filterExplanation).toContainHTML('complex tag expression');
+ expect(window.booru.imagesWithDownvotingDisabled).toContain('1');
+ });
+
+});
+
+describe('initImagesClientside', () => {
+ it('should initialize the imagesWithDownvotingDisabled array', () => {
+ initImagesClientside();
+ expect(window.booru.imagesWithDownvotingDisabled).toEqual([]);
+ });
+});
diff --git a/assets/js/imagesclientside.ts b/assets/js/imagesclientside.ts
index 61e1d1ac..add108c9 100644
--- a/assets/js/imagesclientside.ts
+++ b/assets/js/imagesclientside.ts
@@ -2,6 +2,7 @@
* Client-side image filtering/spoilering.
*/
+import { assertNotUndefined } from './utils/assert';
import { $$, escapeHtml } from './utils/dom';
import { setupInteractions } from './interactions';
import { showThumb, showBlock, spoilerThumb, spoilerBlock, hideThumb } from './utils/image';
@@ -15,20 +16,20 @@ function run(
img: HTMLDivElement,
tags: TagData[],
complex: AstMatcher,
- callback: RunCallback,
+ runCallback: RunCallback
): boolean {
const hit = (() => {
// Check tags array first to provide more precise filter explanations
const hitTags = imageHitsTags(img, tags);
if (hitTags.length !== 0) {
- callback(img, hitTags, 'tags');
+ runCallback(img, hitTags, 'tags');
return true;
}
// No tags matched, try complex filter AST
const hitComplex = imageHitsComplex(img, complex);
if (hitComplex) {
- callback(img, hitTags, 'complex');
+ runCallback(img, hitTags, 'complex');
return true;
}
@@ -36,9 +37,9 @@ function run(
return false;
})();
- if (hit && img.dataset.imageId) {
+ if (hit) {
// Disallow negative interaction on image which is not visible
- window.booru.imagesWithDownvotingDisabled.push(img.dataset.imageId);
+ window.booru.imagesWithDownvotingDisabled.push(assertNotUndefined(img.dataset.imageId));
}
return hit;