• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Freegle / iznik-nuxt3 / c1db7197-804e-45b4-8d43-ff5992ab766b

30 Mar 2026 09:10PM UTC coverage: 44.828% (+1.4%) from 43.397%
c1db7197-804e-45b4-8d43-ff5992ab766b

push

circleci

CircleCI Auto-merge
Auto-merge master to production after successful tests - Original commit: fix: saveEmail takes raw string, not object

4385 of 10026 branches covered (43.74%)

Branch coverage included in aggregate %.

0 of 2 new or added lines in 2 files covered. (0.0%)

454 existing lines in 28 files now uncovered.

1981 of 4175 relevant lines covered (47.45%)

59.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.98
/components/InfiniteLoading.vue
1
<template>
2
  <client-only>
3
    <div
4
      :key="bump"
5
      v-observe-visibility="{
6
        callback: visibilityChanged,
7
        options: observerOptions,
8
      }"
9
      class="infinite-loader"
10
    >
11
      <slot v-if="state == 'loading'" name="spinner"></slot>
450✔
12
      <slot v-if="state == 'complete'" name="complete"></slot>
450✔
13
      <slot v-if="state == 'error'" name="error"></slot>
450!
14
    </div>
15
  </client-only>
16
</template>
17
<script setup>
18
import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
19

20
// Derived from https://github.com/oumoussa98/vue3-infinite-loading.  Reworked radically to allow an async event
21
// handler, and to make consistent with the rest of this codebase.
22
const props = defineProps({
131✔
23
  top: { type: Boolean, required: false },
24
  target: { type: [String, Boolean], required: false, default: null },
25
  distance: { type: Number, required: false, default: 0 },
26
  identifier: { type: [String, Number], required: false, default: null },
27
  firstload: { type: Boolean, required: false, default: true },
28
  slots: { type: Object, required: false, default: null },
29
  forceUseInfiniteWrapper: { type: String, required: false, default: null },
30
})
31

32
// When a scroll wrapper is specified, use it as the IntersectionObserver root.
33
// This is needed when the infinite loader is inside a nested scroll container
34
// (e.g. a chat list with overflow-y: auto) — without this, the observer uses
35
// the document viewport and never detects scrolling within the container.
36
const observerOptions = computed(() => {
37
  const opts = {
131✔
38
    rootMargin: '0px 0px ' + props.distance + 'px 0px',
39
  }
40

41
  if (props.forceUseInfiniteWrapper) {
131✔
42
    const el = document.querySelector(props.forceUseInfiniteWrapper)
14✔
43
    if (el) {
14✔
44
      opts.root = el
45
    }
46
  }
47

48
  return opts
131✔
49
})
50

51
const emit = defineEmits(['infinite'])
52

53
const state = ref('ready')
54
const bump = ref(0)
55
const visible = ref(false)
56
let timer = null
131✔
57

58
// Methods
59
function visibilityChanged(isVisible) {
284✔
60
  visible.value = isVisible
284✔
61
}
62

63
function loading() {
197✔
64
  state.value = 'loading'
197✔
65
}
66

67
function loaded() {
80✔
68
  state.value = 'loaded'
80✔
69
}
70

71
function complete() {
234✔
72
  state.value = 'complete'
234✔
73
}
74

UNCOV
75
function error() {
×
UNCOV
76
  state.value = 'error'
×
77
}
78

79
function stopObserver() {
117✔
80
  complete()
117✔
81
}
82

83
async function emitInfinite() {
197✔
84
  loading()
197✔
85

86
  // Wait for the next tick otherwise if the event handlers return synchronously we may not end up triggering
87
  // the watch.
88
  await nextTick()
89
  emit('infinite', {
90
    loading,
91
    loaded,
92
    complete,
93
    error,
94
    stopObserver,
95
  })
96
}
97

98
function fallback() {
5,351✔
99
  timer = null
5,351✔
100

101
  if (visible.value && state.value === 'loaded') {
10,504✔
102
    // We have loaded and not completed, and yet it's still visible.  We need to do some more.
103
    emitInfinite()
104
  }
105

106
  timer = setTimeout(fallback, 100)
107
}
108

109
// Watch state changes
110
watch(state, (newVal) => {
111
  // console.log('state changed', newVal)
112
  if (newVal === 'loading') {
394✔
113
    // This is an internal change - nothing to do.
114
  } else if (newVal === 'complete') {
197✔
115
    // console.log('Complete, stop observer')
116
    stopObserver()
117✔
117
  } else {
118
    const parentEl = props.target || document.documentElement
80✔
119
    const prevHeight = parentEl.scrollHeight
120

121
    if (newVal === 'loaded' && props.top) {
80!
122
      // console.log('Adjust scrollTop')
123
      parentEl.scrollTop = parentEl.scrollHeight - prevHeight
124
    }
125
  }
126
})
127

128
// Watch identifier changes
129
watch(
130
  () => props.identifier,
131
  () => {
132
    // We've been asked to kick the component to reset it.
133
    bump.value++
8✔
134
    emitInfinite()
135
  }
136
)
137

138
// Lifecycle hooks
139
onMounted(async () => {
140
  if (props.firstload) {
131✔
141
    await emitInfinite()
142
  }
143

144
  // It would be nice if we didn't need a timer and could be purely event-driven.  But there is no guarantee
145
  // that what happens in response to our emit will result in all the components being rendered, and although
146
  // Vue3 has Suspense I can't see an easy way of waiting for all renders to finish.
147
  timer = setTimeout(fallback, 100)
148
})
149

150
onBeforeUnmount(() => {
151
  if (timer) {
62✔
152
    clearTimeout(timer)
153
  }
154
})
155

156
// Expose methods to parent components
157
defineExpose({
158
  loading,
159
  loaded,
160
  complete,
161
  error,
162
  stopObserver,
163
})
164
</script>
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc