@@ -0,0 +1,71 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
function fixture() {
|
||||
const requests = [];
|
||||
const grid = { replaceChildren() {}, appendChild() {} };
|
||||
const context = vm.createContext({ window: {}, URLSearchParams, console: { error() {} },
|
||||
requestAnimationFrame() {}, document: { getElementById: id => id === 'tagPromptsGrid' ? grid : null,
|
||||
querySelector: () => null, createElement: () => ({ appendChild() {} }) },
|
||||
});
|
||||
const Manager = vm.runInContext(fs.readFileSync(path.join(__dirname, '../../web/js/tags-page.js'), 'utf8') + '\nTagsPageManager;', context);
|
||||
const manager = Object.create(Manager.prototype);
|
||||
Object.assign(manager, { selectedTags: ['cats'], filterMode: 'and', promptLimit: 20,
|
||||
promptOffset: 0, isLoadingPrompts: false, prompts: [], hasMorePrompts: false,
|
||||
fetchJson(url) { return new Promise((resolve, reject) => requests.push({ url, resolve, reject })); },
|
||||
renderTagPrompts() {}, appendTagPrompts() {}, updatePromptCountBadge() {}, updateLoadMoreButton() {},
|
||||
});
|
||||
const reply = (index, text, more = false) => requests[index].resolve({ success: true,
|
||||
prompts: [{ text }], pagination: { has_more: more, total: more ? 40 : 1 } });
|
||||
return { manager, requests, reply };
|
||||
}
|
||||
test('changing a filter starts a new request and late results cannot overwrite it', async () => {
|
||||
const { manager, requests, reply } = fixture();
|
||||
const old = manager.loadTagPrompts();
|
||||
manager.selectedTags = ['dogs'];
|
||||
const current = manager.loadTagPrompts();
|
||||
assert.equal(requests.length, 2);
|
||||
assert.match(requests[1].url, /dogs/);
|
||||
reply(1, 'dog prompt'); await current;
|
||||
reply(0, 'cat prompt', true); await old;
|
||||
assert.equal(manager.prompts[0].text, 'dog prompt');
|
||||
assert.equal(manager.promptTotal, 1);
|
||||
assert.equal(manager.hasMorePrompts, false);
|
||||
});
|
||||
test('clearing selection invalidates pending results', async () => {
|
||||
const { manager, reply } = fixture();
|
||||
const old = manager.loadTagPrompts();
|
||||
manager.selectedTags = [];
|
||||
manager.clearPrompts();
|
||||
reply(0, 'cat prompt'); await old;
|
||||
assert.equal(manager.prompts.length, 0);
|
||||
assert.equal(manager.promptTotal, 0);
|
||||
assert.equal(manager.isLoadingPrompts, false);
|
||||
});
|
||||
test('a stale pagination response cannot append rows from the previous filter', async () => {
|
||||
const { manager, requests, reply } = fixture();
|
||||
Object.assign(manager, { prompts: [{ text: 'cat first page' }], hasMorePrompts: true, promptOffset: 20 });
|
||||
const old = manager.loadMoreTagPrompts();
|
||||
manager.selectedTags = ['dogs'];
|
||||
const current = manager.loadTagPrompts();
|
||||
assert.equal(requests.length, 2);
|
||||
reply(1, 'dog prompt'); await current;
|
||||
reply(0, 'cat second page', true); await old;
|
||||
assert.equal(manager.prompts.length, 1);
|
||||
assert.equal(manager.prompts[0].text, 'dog prompt');
|
||||
assert.equal(manager.promptOffset, 20);
|
||||
});
|
||||
test('a stale failure cannot finish the new request or clear its loading state', async () => {
|
||||
const { manager, requests, reply } = fixture();
|
||||
const old = manager.loadTagPrompts();
|
||||
manager.selectedTags = ['dogs'];
|
||||
const current = manager.loadTagPrompts();
|
||||
assert.equal(requests.length, 2);
|
||||
requests[0].reject(new Error('old failure')); await old;
|
||||
assert.equal(manager.isLoadingPrompts, true);
|
||||
reply(1, 'dog prompt'); await current;
|
||||
assert.equal(manager.isLoadingPrompts, false);
|
||||
assert.equal(manager.prompts[0].text, 'dog prompt');
|
||||
});
|
||||
+23
-7
@@ -583,8 +583,12 @@ class TagsPageManager {
|
||||
// ── Prompts Display ───────────────────────────────────────
|
||||
|
||||
async loadTagPrompts() {
|
||||
if (this.isLoadingPrompts) return;
|
||||
if (this.selectedTags.length === 0) return;
|
||||
if (this.selectedTags.length === 0) {
|
||||
this.clearPrompts();
|
||||
return;
|
||||
}
|
||||
const request = {};
|
||||
this.promptRequest = request;
|
||||
|
||||
this.isLoadingPrompts = true;
|
||||
this.promptOffset = 0;
|
||||
@@ -622,6 +626,7 @@ class TagsPageManager {
|
||||
}
|
||||
|
||||
const data = await this.fetchJson(url);
|
||||
if (this.promptRequest !== request) return;
|
||||
|
||||
if (data.success) {
|
||||
this.prompts = data.prompts;
|
||||
@@ -634,6 +639,7 @@ class TagsPageManager {
|
||||
requestAnimationFrame(() => this.refreshPromptObserver());
|
||||
}
|
||||
} catch (err) {
|
||||
if (this.promptRequest !== request) return;
|
||||
console.error('Failed to load tag prompts:', err);
|
||||
if (grid) {
|
||||
grid.replaceChildren();
|
||||
@@ -643,12 +649,14 @@ class TagsPageManager {
|
||||
grid.appendChild(errDiv);
|
||||
}
|
||||
} finally {
|
||||
this.isLoadingPrompts = false;
|
||||
if (this.promptRequest === request) this.isLoadingPrompts = false;
|
||||
}
|
||||
}
|
||||
|
||||
async loadMoreTagPrompts() {
|
||||
if (this.isLoadingPrompts || !this.hasMorePrompts) return;
|
||||
const request = {};
|
||||
this.promptRequest = request;
|
||||
this.isLoadingPrompts = true;
|
||||
|
||||
const spinner = document.querySelector('#tagPromptsLoader .prompts-loader-spinner');
|
||||
@@ -675,6 +683,7 @@ class TagsPageManager {
|
||||
}
|
||||
|
||||
const data = await this.fetchJson(url);
|
||||
if (this.promptRequest !== request) return;
|
||||
|
||||
if (data.success) {
|
||||
const startIndex = this.prompts.length;
|
||||
@@ -685,13 +694,16 @@ class TagsPageManager {
|
||||
this.updatePromptCountBadge();
|
||||
}
|
||||
} catch (err) {
|
||||
if (this.promptRequest !== request) return;
|
||||
console.error('Failed to load more prompts:', err);
|
||||
this.hasMorePrompts = false;
|
||||
} finally {
|
||||
this.isLoadingPrompts = false;
|
||||
if (spinner) spinner.classList.add('hidden');
|
||||
this.updateLoadMoreButton();
|
||||
requestAnimationFrame(() => this.refreshPromptObserver());
|
||||
if (this.promptRequest === request) {
|
||||
this.isLoadingPrompts = false;
|
||||
if (spinner) spinner.classList.add('hidden');
|
||||
this.updateLoadMoreButton();
|
||||
requestAnimationFrame(() => this.refreshPromptObserver());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -987,6 +999,9 @@ class TagsPageManager {
|
||||
}
|
||||
|
||||
clearPrompts() {
|
||||
this.promptRequest = null;
|
||||
this.isLoadingPrompts = false;
|
||||
this.promptOffset = 0;
|
||||
this.prompts = [];
|
||||
this.promptTotal = 0;
|
||||
this.hasMorePrompts = false;
|
||||
@@ -1091,6 +1106,7 @@ class TagsPageManager {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.promptRequest = null;
|
||||
this.destroyInfiniteScroll();
|
||||
this.hideContextMenu();
|
||||
clearTimeout(this.searchTimer);
|
||||
|
||||
Reference in New Issue
Block a user