Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion css/logreader-main.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/* extracted by css-entry-points-plugin */
@import './main-Ckaw_UKe.chunk.css';
@import './main-Br2dmrt8.chunk.css';

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/logreader-main.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/logreader-main.mjs.map

Large diffs are not rendered by default.

102 changes: 102 additions & 0 deletions src/components/table/LogTable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import type { ILogEntry } from '../../interfaces'

import { createTestingPinia } from '@pinia/testing'
import { enableAutoUnmount, mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import LogTable from './LogTable.vue'
import { useLogStore } from '../../store/logging'

/** Browser IntersectionObserver fake: reports visibility on observe() and on every change of it */
class FakeIntersectionObserver {
static visible = true
static instances = new Set<FakeIntersectionObserver>()

static setVisible(visible: boolean) {
if (this.visible !== visible) {
this.visible = visible
this.instances.forEach((observer) => observer.notify())
}
}

constructor(private callback: IntersectionObserverCallback) {}

observe() {
FakeIntersectionObserver.instances.add(this)
this.notify()
}

disconnect() {
FakeIntersectionObserver.instances.delete(this)
}

private notify() {
this.callback([{ isIntersecting: FakeIntersectionObserver.visible } as IntersectionObserverEntry], this as unknown as IntersectionObserver)
}
}

function mountLogTable() {
return mount(LogTable, {
props: { rows: [] },
global: {
plugins: [createTestingPinia({ createSpy: vi.fn, initialState: { 'logreader-settings': { shownLevels: [0, 1, 2, 3, 4] } } })],
stubs: {
LogDetailsModal: true,
LogSearch: true,
LogTableHeader: true,
LogTableRow: true,
},
},
})
}

enableAutoUnmount(afterEach)

describe('LogTable', () => {
beforeEach(() => {
// The table starts empty, so the load-more row is visible
FakeIntersectionObserver.visible = true
FakeIntersectionObserver.instances.clear()
vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver)
})

it('loads more entries when the table is mounted', () => {
mountLogTable()
expect(useLogStore().loadMore).toHaveBeenCalledTimes(1)
})

it('does not load more entries when loaded entries push the load-more row out of view', async () => {
mountLogTable()
const logStore = useLogStore()

FakeIntersectionObserver.setVisible(false)
logStore.allEntries.push({ id: '1', level: 3 } as ILogEntry)
await nextTick()
expect(logStore.loadMore).toHaveBeenCalledTimes(1)
})

it('loads more entries when scrolling makes the load-more row visible again', async () => {
mountLogTable()
const logStore = useLogStore()
FakeIntersectionObserver.setVisible(false)
logStore.allEntries.push({ id: '1', level: 3 } as ILogEntry)
await nextTick()

FakeIntersectionObserver.setVisible(true)
expect(logStore.loadMore).toHaveBeenCalledTimes(2)
})

it('loads more entries again when the load-more row stays visible after loading', async () => {
mountLogTable()
const logStore = useLogStore()

logStore.allEntries.push({ id: '1', level: 3 } as ILogEntry)
await nextTick()
expect(logStore.loadMore).toHaveBeenCalledTimes(2)
})
})
10 changes: 8 additions & 2 deletions src/components/table/LogTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@
<tbody ref="tableBody" :style="tbodyStyle" class="log-table__body">
<tr v-if="sortedByTime === 'ascending'" class="log-table__load-more">
<td>
<IntersectionObserver v-if="logStore.hasRemainingEntries" @intersection="loadMore">
<IntersectionObserver
v-if="logStore.hasRemainingEntries"
:key="logStore.allEntries.length"
@intersection="loadMore">
{{ t('logreader', 'Loading older log entries') }}
</IntersectionObserver>
<span v-else>
Expand All @@ -50,7 +53,10 @@
<tfoot role="rowgroup" class="log-table__footer">
<tr v-if="sortedByTime !== 'ascending'" class="log-table__load-more">
<td>
<IntersectionObserver v-if="logStore.hasRemainingEntries" @intersection="loadMore">
<IntersectionObserver
v-if="logStore.hasRemainingEntries"
:key="logStore.allEntries.length"
@intersection="loadMore">
{{ t('logreader', 'Loading older log entries') }}
</IntersectionObserver>
<span v-else>
Expand Down
7 changes: 7 additions & 0 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ const config = createAppConfig({
reporter: ['lcov', 'text'],
},
environment: 'happy-dom',
unstubGlobals: true,
server: {
deps: {
// Allow importing CSS from dependencies
inline: ['@nextcloud/dialogs', '@nextcloud/vue'],
},
},
},
} as UserConfig,
})
Expand Down
Loading