Skip to content

Commit 055a0d1

Browse files
authored
Merge pull request #23 from CMU-17313Q/anonymous-post-testing
Add anonymous posting tests and integrate automatic testing
2 parents 7b3697f + 9beff00 commit 055a0d1

File tree

3 files changed

+231
-2
lines changed

3 files changed

+231
-2
lines changed

.github/workflows/test.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ jobs:
5353
with:
5454
useLockFile: false
5555

56+
- name: Install chai
57+
run: npm install chai
58+
5659
- name: Install Solved Plugin
5760
run: npm install ./nodebb-plugin-solved
5861

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@
8282
"imagesloaded": "5.0.0",
8383
"ioredis": "5.6.1",
8484
"ipaddr.js": "2.2.0",
85-
"jquery": "3.7.1",
8685
"jquery-deserialize": "2.0.0",
8786
"jquery-form": "4.3.0",
8887
"jquery-serializeobject": "1.0.0",
@@ -101,12 +100,14 @@
101100
"multiparty": "4.2.3",
102101
"nconf": "0.13.0",
103102
"nodebb-plugin-2factor": "7.5.10",
103+
"nodebb-plugin-anonymous-button": "file:nodebb-plugin-anonymous-button",
104104
"nodebb-plugin-composer-default": "10.2.51",
105105
"nodebb-plugin-dbsearch": "6.2.19",
106106
"nodebb-plugin-emoji": "6.0.2",
107107
"nodebb-plugin-emoji-android": "4.1.1",
108108
"nodebb-plugin-markdown": "13.2.1",
109109
"nodebb-plugin-mentions": "4.7.6",
110+
"nodebb-plugin-solved": "file:nodebb-plugin-solved",
110111
"nodebb-plugin-spam-be-gone": "2.3.2",
111112
"nodebb-plugin-summarizer": "file:plugins/nodebb-plugin-thread-summarizer",
112113
"nodebb-plugin-thread-summarizer": "file:nodebb-plugin-thread-summarizer",
@@ -165,6 +166,7 @@
165166
"@commitlint/cli": "19.8.1",
166167
"@commitlint/config-angular": "19.8.1",
167168
"@eslint/js": "9.26.0",
169+
"chai": "^6.2.0",
168170
"@stylistic/eslint-plugin": "^5.4.0",
169171
"coveralls": "3.1.1",
170172
"eslint-config-nodebb": "1.1.5",
@@ -174,7 +176,9 @@
174176
"husky": "8.0.3",
175177
"jest": "^29.7.0",
176178
"jest-environment-jsdom": "^29.7.0",
177-
"jsdom": "26.1.0",
179+
"jquery": "^3.7.1",
180+
"jsdom": "^26.1.0",
181+
"jsdom-global": "^3.0.2",
178182
"lint-staged": "16.0.0",
179183
"mocha": "^10.8.2",
180184
"mocha-lcov-reporter": "1.3.0",

test/anonymous-button.test.js

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
const { expect } = require('chai');
2+
const { JSDOM } = require('jsdom');
3+
const fs = require('fs');
4+
const path = require('path');
5+
6+
describe('Anonymous Button Plugin Test', function () {
7+
let window, document, $;
8+
let hooksCallbacks = {};
9+
10+
beforeEach(function () {
11+
// Create a fresh JSDOM instance for each test
12+
const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
13+
url: 'http://localhost',
14+
runScripts: 'dangerously',
15+
resources: 'usable',
16+
});
17+
18+
window = dom.window;
19+
document = window.document;
20+
21+
// Mock jQuery
22+
$ = function (selector) {
23+
// Handle HTML string
24+
if (typeof selector === 'string' && selector.trim().startsWith('<')) {
25+
const temp = document.createElement('div');
26+
temp.innerHTML = selector;
27+
return $(Array.from(temp.childNodes));
28+
}
29+
30+
// Handle CSS selector string
31+
if (typeof selector === 'string') {
32+
return $(document.querySelectorAll(selector));
33+
}
34+
35+
// Handle array-like (NodeList or Array)
36+
if ((window.NodeList && selector instanceof window.NodeList) || Array.isArray(selector)) {
37+
const elements = Array.from(selector);
38+
const obj = {
39+
length: elements.length,
40+
each: function (callback) {
41+
elements.forEach((el, i) => callback.call(el, i, el));
42+
return obj;
43+
},
44+
on: function (event, handler) {
45+
elements.forEach(el => el.addEventListener(event, handler));
46+
return obj;
47+
},
48+
click: function () {
49+
elements.forEach(el => el.click());
50+
return obj;
51+
},
52+
toggleClass: function (className, toggle) {
53+
elements.forEach(el => {
54+
if (!el.classList) return; // FIX: prevent TypeError
55+
if (toggle === undefined) el.classList.toggle(className);
56+
else if (toggle) el.classList.add(className);
57+
else el.classList.remove(className);
58+
});
59+
return obj;
60+
},
61+
hasClass: function (className) {
62+
return elements.length > 0 && elements[0].classList?.contains(className);
63+
},
64+
addClass: function (className) {
65+
elements.forEach(el => el.classList?.add(className));
66+
return obj;
67+
},
68+
removeClass: function (className) {
69+
elements.forEach(el => el.classList?.remove(className));
70+
return obj;
71+
},
72+
append: function (content) {
73+
elements.forEach(el => {
74+
if (typeof content === 'string') el.insertAdjacentHTML('beforeend', content);
75+
else if (content.jquery) content.each((i, c) => el.appendChild(c));
76+
});
77+
return obj;
78+
},
79+
attr: function (name, value) {
80+
if (value === undefined) return elements.length ? elements[0].getAttribute(name) : undefined;
81+
elements.forEach(el => el.setAttribute(name, value));
82+
return obj;
83+
},
84+
find: function (sel) {
85+
const found = [];
86+
elements.forEach(el => found.push(...el.querySelectorAll(sel)));
87+
return $(found);
88+
},
89+
html: function (content) {
90+
if (content === undefined) return elements.length ? elements[0].innerHTML : '';
91+
elements.forEach(el => el.innerHTML = content);
92+
return obj;
93+
},
94+
replaceWith: function (content) {
95+
elements.forEach(el => {
96+
const temp = document.createElement('div');
97+
temp.innerHTML = content;
98+
el.replaceWith(...temp.childNodes);
99+
});
100+
return obj;
101+
},
102+
text: function () {
103+
return elements.length ? elements[0].textContent : '';
104+
},
105+
first: function () {
106+
return elements.length ? $(elements[0]) : $([]);
107+
},
108+
parent: function () {
109+
const parents = [];
110+
elements.forEach(el => { if (el.parentElement) parents.push(el.parentElement); });
111+
return $(parents);
112+
},
113+
jquery: true,
114+
};
115+
116+
elements.forEach((el, i) => obj[i] = el);
117+
return obj;
118+
}
119+
120+
// Single DOM node
121+
if (selector && selector.nodeType) return $([selector]);
122+
123+
// Fallback
124+
return $(document.querySelectorAll(selector));
125+
};
126+
127+
// Reset hooks
128+
hooksCallbacks = {};
129+
130+
// Mock NodeBB hooks
131+
const hooks = {
132+
on: function (hookName, callback) {
133+
if (!hooksCallbacks[hookName]) hooksCallbacks[hookName] = [];
134+
hooksCallbacks[hookName].push(callback);
135+
},
136+
};
137+
138+
// AMD-style require mock
139+
const amdRequire = function (deps, factory) {
140+
if (Array.isArray(deps) && typeof factory === 'function') {
141+
const mocks = { hooks, api: {} };
142+
const modules = deps.map(d => mocks[d] || {});
143+
factory(...modules);
144+
} else {
145+
return require.apply(this, arguments);
146+
}
147+
};
148+
149+
// Store mocks in window
150+
window.require = amdRequire;
151+
window.$ = $;
152+
window.hooksCallbacks = hooksCallbacks;
153+
});
154+
155+
// Helper function to load plugin
156+
function loadPlugin() {
157+
const pluginCode = fs.readFileSync(
158+
path.join(__dirname, '../nodebb-plugin-anonymous-button/public/js/anonymous-button.js'),
159+
'utf-8'
160+
);
161+
const script = window.document.createElement('script');
162+
script.textContent = pluginCode;
163+
window.document.head.appendChild(script);
164+
}
165+
166+
it('prints to terminal', function () {
167+
console.log('[TEST] Anonymous Button plugin test running!');
168+
expect(true).to.equal(true);
169+
});
170+
171+
it('creates and initializes the plugin hooks', function () {
172+
loadPlugin();
173+
expect(hooksCallbacks['action:ajaxify.end']).to.be.an('array');
174+
expect(hooksCallbacks['action:ajaxify.end'].length).to.be.at.least(1);
175+
});
176+
177+
it('adds the anonymous button when ajaxify.end hook is triggered', function () {
178+
loadPlugin();
179+
hooksCallbacks['action:ajaxify.end']?.forEach(cb => cb());
180+
const button = document.querySelector('#anon-toggle-floating');
181+
expect(button).to.not.be.null;
182+
expect(button.textContent).to.include('Anon');
183+
});
184+
185+
it('button toggles class when clicked', function () {
186+
loadPlugin();
187+
hooksCallbacks['action:ajaxify.end']?.forEach(cb => cb());
188+
const button = document.querySelector('#anon-toggle-floating');
189+
expect(button).to.not.be.null;
190+
191+
expect(button.classList.contains('btn-secondary')).to.be.true;
192+
expect(button.classList.contains('btn-success')).to.be.false;
193+
194+
// Clicking should now toggle properly without TypeError
195+
button.click();
196+
expect(button.classList.contains('btn-success')).to.be.true;
197+
expect(button.classList.contains('btn-secondary')).to.be.false;
198+
199+
button.click();
200+
expect(button.classList.contains('btn-secondary')).to.be.true;
201+
expect(button.classList.contains('btn-success')).to.be.false;
202+
});
203+
204+
it('registers filter:composer.submit hook', function () {
205+
loadPlugin();
206+
expect(hooksCallbacks['filter:composer.submit']).to.be.an('array');
207+
expect(hooksCallbacks['filter:composer.submit'].length).to.be.at.least(1);
208+
});
209+
210+
it('registers action:composer.submit hook', function () {
211+
loadPlugin();
212+
expect(hooksCallbacks['action:composer.submit']).to.be.an('array');
213+
expect(hooksCallbacks['action:composer.submit'].length).to.be.at.least(1);
214+
});
215+
216+
it('button exists only once even when ajaxify.end fires multiple times', function () {
217+
loadPlugin();
218+
for (let i = 0; i < 3; i++) hooksCallbacks['action:ajaxify.end']?.forEach(cb => cb());
219+
const buttons = document.querySelectorAll('#anon-toggle-floating');
220+
expect(buttons.length).to.equal(1);
221+
});
222+
});

0 commit comments

Comments
 (0)