mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-13 20:12:57 -10:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bea7acc18e |
No files matched your search
@@ -3446,6 +3446,10 @@ window.update_window_path = async function (el_window, target_path) {
|
||||
else if ( target_path !== '/' ) {
|
||||
try {
|
||||
puter.fs.stat({ path: target_path, consistency: 'eventual' }).then(fsentry => {
|
||||
// the window may have moved on while this stat was in flight
|
||||
if ( $(el_window).attr('data-path') !== html_encode(target_path) ) {
|
||||
return;
|
||||
}
|
||||
$(el_window).removeClass(`window-${ $(el_window).attr('data-uid')}`);
|
||||
$(el_window).addClass(`window-${ fsentry.id}`);
|
||||
$(el_window).attr('data-uid', fsentry.id);
|
||||
|
||||
@@ -32,6 +32,13 @@ const refresh_item_container = function (el_item_container, options) {
|
||||
options = options || {};
|
||||
|
||||
let container_path = $(el_item_container).attr('data-path');
|
||||
|
||||
// Only the newest refresh of a container may write to it or its window;
|
||||
// an older one's responses belong to a folder the window has since left.
|
||||
const refresh_id = ($(el_item_container).data('refreshId') ?? 0) + 1;
|
||||
$(el_item_container).data('refreshId', refresh_id);
|
||||
const is_current = () => $(el_item_container).data('refreshId') === refresh_id;
|
||||
|
||||
let el_window = $(el_item_container).closest('.window');
|
||||
let el_window_head_icon = $(el_window).find('.window-head-icon');
|
||||
const loading_spinner = $(el_item_container).find('.explorer-loading-spinner');
|
||||
@@ -60,9 +67,7 @@ const refresh_item_container = function (el_item_container, options) {
|
||||
// A timeout that will show the loading spinner if the folder is not loaded
|
||||
// after 1000ms
|
||||
let loading_timeout = setTimeout(function () {
|
||||
// make sure the same folder is still loading
|
||||
if ( $(loading_spinner).closest('.item-container').attr('data-path') !== container_path )
|
||||
{
|
||||
if ( ! is_current() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -89,6 +94,9 @@ const refresh_item_container = function (el_item_container, options) {
|
||||
}
|
||||
|
||||
if ( !is_shared_view ) puter.fs.stat({ path: container_path, consistency: options.consistency ?? 'eventual' }).then(fsentry => {
|
||||
if ( ! is_current() ) {
|
||||
return;
|
||||
}
|
||||
if ( el_window ) {
|
||||
$(el_window).attr('data-uid', fsentry.id);
|
||||
$(el_window).attr('data-sort_by', fsentry.sort_by ?? 'name');
|
||||
@@ -156,10 +164,8 @@ const refresh_item_container = function (el_item_container, options) {
|
||||
: puter.fs.readdir({ path: container_path, consistency: options.consistency ?? 'eventual' });
|
||||
|
||||
entries_promise.then(async (fsentries) => {
|
||||
// Check if the same folder is still loading since el_item_container's
|
||||
// data-path might have changed by other operations while waiting for the response to this `readdir`.
|
||||
if ( $(el_item_container).attr('data-path') !== container_path )
|
||||
{
|
||||
if ( ! is_current() ) {
|
||||
clearTimeout(loading_timeout);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -168,6 +174,11 @@ const refresh_item_container = function (el_item_container, options) {
|
||||
// clear loading timeout
|
||||
clearTimeout(loading_timeout);
|
||||
|
||||
// a newer refresh may have started during the spinner delay
|
||||
if ( ! is_current() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// hide loading spinner
|
||||
$(loading_spinner).hide();
|
||||
|
||||
@@ -187,6 +198,10 @@ const refresh_item_container = function (el_item_container, options) {
|
||||
|
||||
// add each item to window
|
||||
for ( let index = 0; index < fsentries.length; index++ ) {
|
||||
// item_icon() is awaited below, so a newer refresh can start mid-loop
|
||||
if ( ! is_current() ) {
|
||||
return;
|
||||
}
|
||||
const fsentry = fsentries[index];
|
||||
let is_disabled = false;
|
||||
|
||||
@@ -258,6 +273,9 @@ const refresh_item_container = function (el_item_container, options) {
|
||||
if ( $(el_item_container).hasClass('desktop') ) {
|
||||
try {
|
||||
const trash = await puter.fs.stat({ path: window.trash_path, consistency: options.consistency ?? 'eventual' });
|
||||
if ( ! is_current() ) {
|
||||
return;
|
||||
}
|
||||
UIItem({
|
||||
appendTo: el_item_container,
|
||||
uid: trash.id,
|
||||
@@ -319,6 +337,11 @@ const refresh_item_container = function (el_item_container, options) {
|
||||
// clear loading timeout
|
||||
clearTimeout(loading_timeout);
|
||||
|
||||
// don't show a folder's error in the folder the window moved to
|
||||
if ( ! is_current() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// hide other messages/indicators
|
||||
$(loading_spinner).hide();
|
||||
$(empty_message).hide();
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import jQuery from '../lib/jquery-3.6.1/jquery-3.6.1.min.js';
|
||||
|
||||
vi.mock('../UI/UIItem.js', () => ({ default: vi.fn() }));
|
||||
@@ -30,6 +30,11 @@ vi.mock('./listAllShared.js', () => ({ default: vi.fn() }));
|
||||
globalThis.$ = jQuery;
|
||||
globalThis.jQuery = jQuery;
|
||||
globalThis.i18n = (key) => key;
|
||||
globalThis.html_encode = (value) =>
|
||||
jQuery('<div>')
|
||||
.text(value ?? '')
|
||||
.html();
|
||||
globalThis.puter = { fs: { stat: vi.fn(), readdir: vi.fn() } };
|
||||
jQuery.fn.removeItems = function () {
|
||||
return this.remove();
|
||||
};
|
||||
@@ -46,6 +51,8 @@ Object.assign(window, {
|
||||
check_fsentry_against_allowed_file_types_string: () => true,
|
||||
sort_items: () => {},
|
||||
update_explorer_footer_item_count: () => {},
|
||||
update_window_layout: () => {},
|
||||
update_details_layout_sort_visuals: () => {},
|
||||
});
|
||||
|
||||
const { default: refresh_item_container } =
|
||||
@@ -53,6 +60,10 @@ const { default: refresh_item_container } =
|
||||
const { default: UIItem } = await import('../UI/UIItem.js');
|
||||
const { default: list_all_shared } = await import('./listAllShared.js');
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('refresh_item_container in the Shared view', () => {
|
||||
it('flags rows shared through a team, and only those', async () => {
|
||||
list_all_shared.mockResolvedValue([
|
||||
@@ -97,3 +108,91 @@ describe('refresh_item_container in the Shared view', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const deferred = () => {
|
||||
let resolve;
|
||||
const promise = new Promise((r) => {
|
||||
resolve = r;
|
||||
});
|
||||
return { promise, resolve };
|
||||
};
|
||||
|
||||
const fileEntry = (dir, name) => ({
|
||||
uid: `${dir}/${name}`,
|
||||
name,
|
||||
path: `${dir}/${name}`,
|
||||
is_dir: false,
|
||||
});
|
||||
|
||||
// Points the window at a new folder the way update_window_path does, then refreshes it.
|
||||
const navigate = (path) => {
|
||||
$('.window').attr('data-path', path);
|
||||
$('.item-container').attr('data-path', path);
|
||||
refresh_item_container($('.item-container'));
|
||||
};
|
||||
|
||||
const renderedPaths = () => UIItem.mock.calls.map(([options]) => options.path);
|
||||
|
||||
describe('refresh_item_container after the window moves to another folder', () => {
|
||||
let pending;
|
||||
|
||||
beforeEach(() => {
|
||||
pending = { stat: {}, readdir: {} };
|
||||
puter.fs.stat.mockImplementation(({ path }) => {
|
||||
pending.stat[path] = deferred();
|
||||
return pending.stat[path].promise;
|
||||
});
|
||||
puter.fs.readdir.mockImplementation(({ path }) => {
|
||||
pending.readdir[path] ??= deferred();
|
||||
return pending.readdir[path].promise;
|
||||
});
|
||||
document.body.innerHTML =
|
||||
'<div class="window"><div class="item-container"></div></div>';
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('ignores a stat for the folder it left', async () => {
|
||||
navigate('/alice');
|
||||
navigate('/alice/Pictures');
|
||||
|
||||
pending.stat['/alice/Pictures'].resolve({ id: 'pictures', name: 'Pictures' });
|
||||
pending.stat['/alice'].resolve({ id: 'home', name: 'alice' });
|
||||
await vi.waitFor(() => expect($('.window').attr('data-uid')).toBe('pictures'));
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
|
||||
expect($('.window').attr('data-path')).toBe('/alice/Pictures');
|
||||
expect($('.window').attr('data-uid')).toBe('pictures');
|
||||
});
|
||||
|
||||
it('drops a listing that arrives just before the window moves on', async () => {
|
||||
vi.useFakeTimers();
|
||||
navigate('/alice/Documents');
|
||||
|
||||
// A listing slower than 1s waits another second before rendering.
|
||||
await vi.advanceTimersByTimeAsync(1100);
|
||||
pending.readdir['/alice/Documents'].resolve([fileEntry('/alice/Documents', 'a.txt')]);
|
||||
await vi.advanceTimersByTimeAsync(0);
|
||||
|
||||
navigate('/alice/Pictures');
|
||||
pending.readdir['/alice/Pictures'].resolve([fileEntry('/alice/Pictures', 'b.png')]);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
expect(renderedPaths()).toEqual(['/alice/Pictures/b.png']);
|
||||
});
|
||||
|
||||
it('renders a folder once when it is refreshed again before the first listing arrives', async () => {
|
||||
navigate('/alice/Documents');
|
||||
navigate('/alice/Pictures');
|
||||
navigate('/alice/Documents');
|
||||
|
||||
pending.readdir['/alice/Documents'].resolve([fileEntry('/alice/Documents', 'a.txt')]);
|
||||
pending.readdir['/alice/Pictures'].resolve([fileEntry('/alice/Pictures', 'b.png')]);
|
||||
await vi.waitFor(() => expect(UIItem).toHaveBeenCalled());
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
|
||||
expect(renderedPaths()).toEqual(['/alice/Documents/a.txt']);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user