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 client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This directory contains the code for:

## Development

Requires Node 18+.
Requires Node 20+.

``` bash
# install dependencies
Expand Down
11 changes: 7 additions & 4 deletions client/dive-common/components/ImportAnnotations.vue
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,18 @@ export default defineComponent({
}
}
});
// inject() only resolves while a component instance is current, which
// outside setup() holds solely during render. Resolve the annotation sets
// here so the computed below can be evaluated from anywhere.
const annotationSets = useAnnotationSets();
const sets = computed(() => {
const data = useAnnotationSets();
const temp = cloneDeep(data.value);
const temp = cloneDeep(annotationSets.value);
temp.push('default');
return temp;
});
const defaultSet = useAnnotationSet();
const currentSet = ref(defaultSet || 'default');
// Local copy (not an alias of the injected ref); '' means default.
const currentSet = ref(defaultSet.value || 'default');
const { prompt } = usePrompt();
const processing = ref(false);
const menuOpen = ref(false);
Expand Down Expand Up @@ -472,7 +476,6 @@ export default defineComponent({
</v-btn>
</v-row>
<v-row
v-if="currentSet !== ''"
class="mt-3"
dense
>
Expand Down
87 changes: 87 additions & 0 deletions client/dive-common/components/importAnnotationsSets.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// @vitest-environment jsdom
import { defineComponent, h, ref } from 'vue';
// eslint-disable-next-line import/no-extraneous-dependencies -- @vue/test-utils is only used in tests
import { mount } from '@vue/test-utils';
import { provideAnnotator, dummyState, dummyHandler } from 'vue-media-annotator/provides';
import { provideApi } from 'dive-common/apispec';
import ImportAnnotations from './ImportAnnotations.vue';

vi.mock('dive-common/vue-utilities/prompt-service', () => ({
usePrompt: () => ({ prompt: vi.fn() }),
}));

/**
* The Import menu's contents are lazy: v-menu does not render them until the
* menu is opened, so `sets` is first evaluated from outside a render pass.
* inject() only resolves while a component instance is current -- which,
* outside setup(), holds solely during render -- so resolving the annotation
* sets inside the computed threw "Missing provided object for symbol
* Symbol(annotationSets)" for every such caller. Mounting with the menu closed
* reproduces that; touching `sets` here must not throw.
*/
function mountImportAnnotations(annotationSets: string[], annotationSet = '') {
const state = dummyState();
state.annotationSets = ref(annotationSets);
state.annotationSet = ref(annotationSet);

const Parent = defineComponent({
setup() {
provideApi({
openFromDisk: vi.fn(),
importAnnotationFile: vi.fn(),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any);
provideAnnotator(
state,
dummyHandler(() => {}),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
{} as any,
);
return () => h(ImportAnnotations, { props: { datasetId: 'dataset-1' } });
},
});

const wrapper = mount(Parent, {
stubs: {
'v-menu': true,
'v-tooltip': true,
'v-card': true,
'v-btn': true,
'v-icon': true,
},
mocks: { $vuetify: { breakpoint: { mdAndDown: false } } },
});
return { vm: wrapper.findComponent(ImportAnnotations), state };
}

describe('ImportAnnotations annotation sets', () => {
it('exposes sets without requiring a render pass', () => {
const { vm } = mountImportAnnotations(['setA', 'setB']);
// Reading from outside render is what regressed: it must not throw.
expect(() => vm.vm.sets).not.toThrow();
expect(vm.vm.sets).toEqual(['setA', 'setB', 'default']);
});

it('always offers the default set when none are defined', () => {
const { vm } = mountImportAnnotations([]);
expect(vm.vm.sets).toEqual(['default']);
});

it('does not mutate the provided annotation sets', () => {
const provided = ['only'];
const { vm } = mountImportAnnotations(provided);
expect(vm.vm.sets).toEqual(['only', 'default']);
expect(provided).toEqual(['only']);
});

it('seeds currentSet from the provided annotation set, falling back to default', () => {
expect(mountImportAnnotations([], '').vm.vm.currentSet).toBe('default');
expect(mountImportAnnotations(['a'], 'a').vm.vm.currentSet).toBe('a');
});

it('does not alias the injected annotationSet ref', () => {
const { vm, state } = mountImportAnnotations(['a'], 'a');
vm.vm.currentSet = 'other';
expect(state.annotationSet.value).toBe('a');
});
});
Loading