Browse Source

test: add playwright test

Signed-off-by: Pranav C <pranavxc@gmail.com>
pull/4406/head
Pranav C 2 years ago
parent
commit
b4b63e5140
  1. 76
      packages/nc-gui/components/cell/MultiSelect.vue
  2. 72
      packages/nc-gui/components/cell/SingleSelect.vue
  3. 33
      tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts
  4. 25
      tests/playwright/tests/columnMultiSelect.spec.ts
  5. 13
      tests/playwright/tests/columnSingleSelect.spec.ts

76
packages/nc-gui/components/cell/MultiSelect.vue

@ -8,14 +8,17 @@ import {
IsKanbanInj, IsKanbanInj,
ReadonlyInj, ReadonlyInj,
computed, computed,
enumColor,
h, h,
inject, inject,
onMounted, onMounted,
reactive,
ref, ref,
useEventListener, useEventListener,
useMetas,
useProject, useProject,
useSelectedCellKeyupListener, useSelectedCellKeyupListener,
watch, enumColor, useMetas, watch,
} from '#imports' } from '#imports'
import MdiCloseCircle from '~icons/mdi/close-circle' import MdiCloseCircle from '~icons/mdi/close-circle'
@ -28,8 +31,6 @@ const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const emptyOption = Symbol('emptyOption')
const { isMysql } = useProject() const { isMysql } = useProject()
const column = inject(ColumnInj)! const column = inject(ColumnInj)!
@ -54,11 +55,7 @@ const { $api } = useNuxtApp()
const { getMeta } = useMetas() const { getMeta } = useMetas()
const tempVal = ref<string>() const tempVal = reactive([])
const isOptionMissing = computed(() => {
return (options.value ?? []).every((op) => op.title !== searchVal.value)
})
const options = computed<(SelectOptionType & { value?: string })[]>(() => { const options = computed<(SelectOptionType & { value?: string })[]>(() => {
if (column?.value.colOptions) { if (column?.value.colOptions) {
@ -73,6 +70,10 @@ const options = computed<(SelectOptionType & { value?: string })[]>(() => {
return [] return []
}) })
const isOptionMissing = computed(() => {
return (options.value ?? []).every((op) => op.title !== searchVal.value)
})
const vModel = computed({ const vModel = computed({
get: () => { get: () => {
const selected = selectedIds.value.reduce((acc, id) => { const selected = selectedIds.value.reduce((acc, id) => {
@ -83,7 +84,7 @@ const vModel = computed({
return acc return acc
}, [] as string[]) }, [] as string[])
if (tempVal.value) selected.push(tempVal.value) if (tempVal.length) selected.push(...tempVal)
return selected return selected
}, },
@ -112,18 +113,18 @@ const selectedTitles = computed(() =>
: [], : [],
) )
const handleKeys = async (e: KeyboardEvent) => { // const handleKeys = async (e: KeyboardEvent) => {
switch (e.key) { // switch (e.key) {
case 'Escape': // case 'Escape':
e.preventDefault() // e.preventDefault()
isOpen.value = false // isOpen.value = false
break // break
case 'Enter': // case 'Enter':
e.stopPropagation() // e.stopPropagation()
await addIfMissingAndSave() // await addIfMissingAndSave()
break // break
} // }
} // }
const handleClose = (e: MouseEvent) => { const handleClose = (e: MouseEvent) => {
if (aselect.value && !aselect.value.$el.contains(e.target)) { if (aselect.value && !aselect.value.$el.contains(e.target)) {
@ -177,20 +178,26 @@ useSelectedCellKeyupListener(active, (e) => {
isOpen.value = true isOpen.value = true
} }
break break
default:
isOpen.value = true
break
} }
}) })
const activeOptCreateInProgress = ref(0)
async function addIfMissingAndSave() { async function addIfMissingAndSave() {
if (!searchVal) return false if (!searchVal) return false
try { try {
tempVal.value = searchVal.value tempVal.push(searchVal.value)
const newOptValue = searchVal?.value const newOptValue = searchVal?.value
searchVal.value = ''
activeOptCreateInProgress.value++
if (newOptValue && !options.value.some((o) => o.title === newOptValue)) { if (newOptValue && !options.value.some((o) => o.title === newOptValue)) {
const newOptions = [...options.value] const newOptions = [...options.value]
newOptions.push({ newOptions.push({
title: newOptValue, value: newOptValue, title: newOptValue,
value: newOptValue,
color: enumColor.light[(options.value.length + 1) % enumColor.light.length], color: enumColor.light[(options.value.length + 1) % enumColor.light.length],
}) })
column.value.colOptions = { options: newOptions.map(({ value: _, ...rest }) => rest) } column.value.colOptions = { options: newOptions.map(({ value: _, ...rest }) => rest) }
@ -198,20 +205,25 @@ async function addIfMissingAndSave() {
await $api.dbTableColumn.update(column.value?.id as string, { await $api.dbTableColumn.update(column.value?.id as string, {
...column.value, ...column.value,
}) })
await getMeta(column.value.fk_model_id!, true)
vModel.value = [...vModel.value] activeOptCreateInProgress.value--
searchVal.value = '' if (!activeOptCreateInProgress.value) {
await getMeta(column.value.fk_model_id!, true)
vModel.value = [...vModel.value];
tempVal.splice(0, tempVal.length);
}
} else {
activeOptCreateInProgress.value--
} }
} catch (e) { } catch (e) {
// todo: handle error message // todo: handle error message
console.log(e) console.log(e)
activeOptCreateInProgress.value--
} finally { } finally {
tempVal.value = '' // tempVal.value = ''
} }
} }
const search = () => { const search = () => {
searchVal.value = aselect.value?.$el?.querySelector('.ant-select-selection-search-input')?.value searchVal.value = aselect.value?.$el?.querySelector('.ant-select-selection-search-input')?.value
} }
@ -226,7 +238,7 @@ const search = () => {
class="w-full" class="w-full"
:bordered="false" :bordered="false"
:show-arrow="!readOnly" :show-arrow="!readOnly"
show-search :show-search="active || editable"
:open="isOpen" :open="isOpen"
:disabled="readOnly" :disabled="readOnly"
:class="{ '!ml-[-8px]': readOnly }" :class="{ '!ml-[-8px]': readOnly }"
@ -260,7 +272,9 @@ const search = () => {
<a-select-option v-if="searchVal && isOptionMissing" :key="searchVal" :value="searchVal"> <a-select-option v-if="searchVal && isOptionMissing" :key="searchVal" :value="searchVal">
<div class="flex gap-2 text-gray-500 items-center"> <div class="flex gap-2 text-gray-500 items-center">
<MdiPlusThick class="min-w-4" /> <MdiPlusThick class="min-w-4" />
<div class="text-xs whitespace-normal"> Create new option named <strong>{{ searchVal }}</strong></div> <div class="text-xs whitespace-normal">
Create new option named <strong>{{ searchVal }}</strong>
</div>
</div> </div>
</a-select-option> </a-select-option>

72
packages/nc-gui/components/cell/SingleSelect.vue

@ -10,11 +10,11 @@ import {
IsKanbanInj, IsKanbanInj,
ReadonlyInj, ReadonlyInj,
computed, computed,
enumColor,
inject, inject,
ref, ref,
useEventListener, useEventListener,
watch, watch,
enumColor,
} from '#imports' } from '#imports'
interface Props { interface Props {
@ -45,6 +45,20 @@ const { $api } = useNuxtApp()
const searchVal = ref() const searchVal = ref()
const tempVal = ref<string>() const tempVal = ref<string>()
const options = computed<(SelectOptionType & { value: string })[]>(() => {
if (column?.value.colOptions) {
const opts = column.value.colOptions
? // todo: fix colOptions type, options does not exist as a property
(column.value.colOptions as any).options.filter((el: SelectOptionType) => el.title !== '') || []
: []
for (const op of opts.filter((el: any) => el.order === null)) {
op.title = op.title.replace(/^'/, '').replace(/'$/, '')
}
return opts.map((o: any) => ({ ...o, value: o.title }))
}
return []
})
const isOptionMissing = computed(() => { const isOptionMissing = computed(() => {
return (options.value ?? []).every((op) => op.title !== searchVal.value) return (options.value ?? []).every((op) => op.title !== searchVal.value)
}) })
@ -62,36 +76,21 @@ const vModel = computed({
}, },
}) })
// const handleKeys = async (e: KeyboardEvent) => {
const options = computed<(SelectOptionType & { value: string })[]>(() => { // switch (e.key) {
if (column?.value.colOptions) { // case 'Escape':
const opts = column.value.colOptions // e.preventDefault()
? // todo: fix colOptions type, options does not exist as a property // isOpen.value = false
(column.value.colOptions as any).options.filter((el: SelectOptionType) => el.title !== '') || [] // break
: [] // case 'Enter':
for (const op of opts.filter((el: any) => el.order === null)) { // e.preventDefault()
op.title = op.title.replace(/^'/, '').replace(/'$/, '') // // if (await addIfMissingAndSave())
} // // e.stopPropagation()
return opts.map((o: any) => ({ ...o, value: o.title })) // break
} // }
return [] // }
})
const handleClose = (_e: MouseEvent) => {
const handleKeys = async (e: KeyboardEvent) => {
switch (e.key) {
case 'Escape':
e.preventDefault()
isOpen.value = false
break
case 'Enter':
e.preventDefault()
// if (await addIfMissingAndSave())
// e.stopPropagation()
break
}
}
const handleClose = (e: MouseEvent) => {
// if (aselect.value && !aselect.value.$el.contains(e.target)) { // if (aselect.value && !aselect.value.$el.contains(e.target)) {
// isOpen.value = false // isOpen.value = false
// aselect.value.blur() // aselect.value.blur()
@ -121,9 +120,7 @@ useSelectedCellKeyupListener(active, (e) => {
} }
}) })
async function addIfMissingAndSave() { async function addIfMissingAndSave() {
if (!searchVal.value) return false if (!searchVal.value) return false
const newOptValue = searchVal.value const newOptValue = searchVal.value
@ -144,7 +141,6 @@ async function addIfMissingAndSave() {
} }
} }
const search = () => { const search = () => {
searchVal.value = aselect.value?.$el?.querySelector('.ant-select-selection-search-input')?.value searchVal.value = aselect.value?.$el?.querySelector('.ant-select-selection-search-input')?.value
} }
@ -161,9 +157,9 @@ const search = () => {
:disabled="readOnly" :disabled="readOnly"
:show-arrow="!readOnly && (active || editable || vModel === null)" :show-arrow="!readOnly && (active || editable || vModel === null)"
:dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen ? 'active' : ''}`" :dropdown-class-name="`nc-dropdown-single-select-cell ${isOpen ? 'active' : ''}`"
:show-search="active || editable"
@select="isOpen = false" @select="isOpen = false"
@keydown.stop @keydown.stop
show-search
@search="search" @search="search"
@click="isOpen = (active || editable) && !isOpen" @click="isOpen = (active || editable) && !isOpen"
> >
@ -189,14 +185,14 @@ const search = () => {
</a-tag> </a-tag>
</a-select-option> </a-select-option>
<a-select-option v-if="searchVal && isOptionMissing" :key="searchVal" :value="searchVal"> <a-select-option v-if="searchVal && isOptionMissing" :key="searchVal" :value="searchVal">
<div class="flex gap-2 text-gray-500 items-center"> <div class="flex gap-2 text-gray-500 items-center">
<MdiPlusThick class="min-w-4" /> <MdiPlusThick class="min-w-4" />
<div class="text-xs whitespace-normal"> Create new option named <strong>{{ searchVal }}</strong></div> <div class="text-xs whitespace-normal">
Create new option named <strong>{{ searchVal }}</strong>
</div>
</div> </div>
</a-select-option> </a-select-option>
</a-select> </a-select>
</template> </template>

33
tests/playwright/pages/Dashboard/common/Cell/SelectOptionCell.ts

@ -112,4 +112,37 @@ export class SelectOptionCellPageObject extends BasePage {
await this.get({ index, columnHeader }).click(); await this.get({ index, columnHeader }).click();
await this.rootPage.locator(`.nc-dropdown-single-select-cell`).nth(index).waitFor({ state: 'hidden' }); await this.rootPage.locator(`.nc-dropdown-single-select-cell`).nth(index).waitFor({ state: 'hidden' });
} }
async addNewOption({
index,
columnHeader,
option,
multiSelect,
}: {
index: number;
columnHeader: string;
option: string;
multiSelect?: boolean;
}) {
const selectCell = this.get({ index, columnHeader });
// check if cell active
if (!(await selectCell.getAttribute('class')).includes('active')) {
await selectCell.click();
}
await selectCell.locator('.ant-select-selection-search-input').type(option);
await selectCell.locator('.ant-select-selection-search-input').press('Enter');
if (multiSelect) await selectCell.locator('.ant-select-selection-search-input').press('Escape');
// await this.rootPage.getByTestId(`select-option-${columnHeader}-${index}`).getByText(option).click();
//
//
// await this.rootPage
// .getByTestId(`select-option-${columnHeader}-${index}`)
// .getByText(option)
// .waitFor({ state: 'hidden' });
}
} }

25
tests/playwright/tests/columnMultiSelect.spec.ts

@ -128,4 +128,29 @@ test.describe('Multi select', () => {
await grid.column.delete({ title: 'MultiSelect' }); await grid.column.delete({ title: 'MultiSelect' });
}); });
test.only('Add new option directly from cell', async () => {
await grid.cell.selectOption.addNewOption({
index: 0,
columnHeader: 'MultiSelect',
option: 'Option added from cell 1',
multiSelect: true,
});
await grid.cell.selectOption.addNewOption({
index: 0,
columnHeader: 'MultiSelect',
option: 'Option added from cell 2',
multiSelect: true,
});
await grid.cell.selectOption.verifyOptions({
index: 0,
columnHeader: 'MultiSelect',
options: ['Option added from cell 1', 'Option added from cell 2'],
});
await new Promise(resolve => setTimeout(resolve, 100000));
await grid.column.delete({ title: 'SingleSelect' });
});
}); });

13
tests/playwright/tests/columnSingleSelect.spec.ts

@ -69,4 +69,17 @@ test.describe('Single select', () => {
await grid.column.delete({ title: 'SingleSelect' }); await grid.column.delete({ title: 'SingleSelect' });
}); });
test('Add new option directly from cell', async () => {
await grid.cell.selectOption.addNewOption({
index: 0,
columnHeader: 'SingleSelect',
option: 'Option added from cell',
});
await grid.cell.selectOption.verify({ index: 0, columnHeader: 'SingleSelect', option: 'Option added from cell' });
await new Promise(resolve => setTimeout(resolve, 100000));
await grid.column.delete({ title: 'SingleSelect' });
});
}); });

Loading…
Cancel
Save