snapshot of https://github.com/frappe/crm.git (develop, 2026-09-18; blobs > 131072B omitted)
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
Some checks failed
Frontend / Unit Tests & Coverage (push) Has been skipped
Regenerate POT file (translatable strings) / Regenerate POT file (develop) (push) Failing after 1m13s
Regenerate POT file (translatable strings) / Regenerate POT file (main-hotfix) (push) Failing after 6s
Server / Tests (frappe ${{ matrix.frappe-branch }}) (push) Has been skipped
Server / Resolve frappe matrix (push) Failing after 1s
Frontend / Lint (push) Failing after 1s
Frontend / Production Build (push) Failing after 1s
Server / Coverage Wrap Up (push) Has been skipped
Frontend / Coverage Wrap Up (push) Has been skipped
This commit is contained in:
405
frontend/tests/unit/fieldLayoutTabState.test.js
Normal file
405
frontend/tests/unit/fieldLayoutTabState.test.js
Normal file
@@ -0,0 +1,405 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { createApp, defineComponent, h, nextTick, ref } from 'vue'
|
||||
|
||||
vi.mock('frappe-ui', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
|
||||
return {
|
||||
Tabs: defineComponent({
|
||||
props: {
|
||||
modelValue: { type: Number, default: 0 },
|
||||
tabs: { type: Array, default: () => [] },
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
setup(props, { emit, slots }) {
|
||||
return () =>
|
||||
h('div', [
|
||||
h(
|
||||
'div',
|
||||
{ role: 'tablist' },
|
||||
props.tabs.map((tab, index) =>
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
role: 'tab',
|
||||
'aria-selected': props.modelValue === index,
|
||||
onClick: () => emit('update:modelValue', index),
|
||||
},
|
||||
tab.label,
|
||||
),
|
||||
),
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{ role: 'tabpanel' },
|
||||
slots['tab-panel']?.({ tab: props.tabs[props.modelValue] }),
|
||||
),
|
||||
])
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/data/document', () => ({
|
||||
useDocument: () => ({
|
||||
document: {
|
||||
fieldPropertyOverrides: {},
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/components/FieldLayout/Section.vue', () => ({
|
||||
default: defineComponent({
|
||||
props: {
|
||||
section: { type: Object, required: true },
|
||||
},
|
||||
setup(props) {
|
||||
return () => h('section', props.section.label)
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('FieldLayout tab state', () => {
|
||||
it('keeps the selected data field tab when the layout remounts', async () => {
|
||||
const { default: FieldLayout } = await import(
|
||||
'@/components/FieldLayout/FieldLayout.vue'
|
||||
)
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
|
||||
const Parent = defineComponent({
|
||||
setup() {
|
||||
const showLayout = ref(true)
|
||||
const fieldLayoutTabIndex = ref(0)
|
||||
const tabs = [
|
||||
{
|
||||
name: 'details_tab',
|
||||
label: 'Details',
|
||||
sections: [{ name: 'details_section', label: 'Details' }],
|
||||
},
|
||||
{
|
||||
name: 'data_tab',
|
||||
label: 'Data',
|
||||
sections: [{ name: 'data_section', label: 'Data' }],
|
||||
},
|
||||
]
|
||||
|
||||
return () =>
|
||||
h('div', [
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
'data-testid': 'remount',
|
||||
onClick: async () => {
|
||||
showLayout.value = false
|
||||
await nextTick()
|
||||
showLayout.value = true
|
||||
},
|
||||
},
|
||||
'Remount',
|
||||
),
|
||||
showLayout.value &&
|
||||
h(FieldLayout, {
|
||||
tabs,
|
||||
data: {},
|
||||
doctype: 'CRM Deal',
|
||||
isGridRow: true,
|
||||
tabIndex: fieldLayoutTabIndex.value,
|
||||
'onUpdate:tabIndex': (value) => {
|
||||
fieldLayoutTabIndex.value = value
|
||||
},
|
||||
}),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
const app = createApp(Parent)
|
||||
app.mount(root)
|
||||
|
||||
root.querySelectorAll('[role="tab"]')[1].click()
|
||||
await nextTick()
|
||||
expect(root.querySelector('[aria-selected="true"]').textContent).toBe(
|
||||
'Data',
|
||||
)
|
||||
|
||||
root.querySelector('[data-testid="remount"]').click()
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(root.querySelector('[aria-selected="true"]').textContent).toBe(
|
||||
'Data',
|
||||
)
|
||||
|
||||
app.unmount()
|
||||
root.remove()
|
||||
})
|
||||
|
||||
it('keeps the selected data field tab when DataFields remounts', async () => {
|
||||
const { default: FieldLayout } = await import(
|
||||
'@/components/FieldLayout/FieldLayout.vue'
|
||||
)
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
|
||||
const DataFieldsStub = defineComponent({
|
||||
props: {
|
||||
fieldLayoutTabIndex: { type: Number, default: 0 },
|
||||
},
|
||||
emits: ['update:fieldLayoutTabIndex'],
|
||||
setup(props, { emit }) {
|
||||
const tabs = [
|
||||
{
|
||||
name: 'details_tab',
|
||||
label: 'Details',
|
||||
sections: [{ name: 'details_section', label: 'Details' }],
|
||||
},
|
||||
{
|
||||
name: 'data_tab',
|
||||
label: 'Data',
|
||||
sections: [{ name: 'data_section', label: 'Data' }],
|
||||
},
|
||||
]
|
||||
|
||||
return () =>
|
||||
h(FieldLayout, {
|
||||
tabs,
|
||||
data: {},
|
||||
doctype: 'CRM Deal',
|
||||
isGridRow: true,
|
||||
tabIndex: props.fieldLayoutTabIndex,
|
||||
'onUpdate:tabIndex': (value) => {
|
||||
emit('update:fieldLayoutTabIndex', value)
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const Parent = defineComponent({
|
||||
setup() {
|
||||
const showDataFields = ref(true)
|
||||
const fieldLayoutTabIndex = ref(0)
|
||||
|
||||
return () =>
|
||||
h('div', [
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
'data-testid': 'remount-data-fields',
|
||||
onClick: async () => {
|
||||
showDataFields.value = false
|
||||
await nextTick()
|
||||
showDataFields.value = true
|
||||
},
|
||||
},
|
||||
'Remount DataFields',
|
||||
),
|
||||
showDataFields.value &&
|
||||
h(DataFieldsStub, {
|
||||
fieldLayoutTabIndex: fieldLayoutTabIndex.value,
|
||||
'onUpdate:fieldLayoutTabIndex': (value) => {
|
||||
fieldLayoutTabIndex.value = value
|
||||
},
|
||||
}),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
const app = createApp(Parent)
|
||||
app.mount(root)
|
||||
|
||||
root.querySelectorAll('[role="tab"]')[1].click()
|
||||
await nextTick()
|
||||
expect(root.querySelector('[aria-selected="true"]').textContent).toBe(
|
||||
'Data',
|
||||
)
|
||||
|
||||
root.querySelector('[data-testid="remount-data-fields"]').click()
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(root.querySelector('[aria-selected="true"]').textContent).toBe(
|
||||
'Data',
|
||||
)
|
||||
|
||||
app.unmount()
|
||||
root.remove()
|
||||
})
|
||||
|
||||
it('keeps the selected tab by name when labels are duplicated and tabs reload', async () => {
|
||||
const { default: FieldLayout } = await import(
|
||||
'@/components/FieldLayout/FieldLayout.vue'
|
||||
)
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
|
||||
const Parent = defineComponent({
|
||||
setup() {
|
||||
const fieldLayoutTabIndex = ref(0)
|
||||
const fieldLayoutTabName = ref('')
|
||||
const tabs = ref([
|
||||
{
|
||||
name: 'tab_a',
|
||||
label: 'New Tab',
|
||||
sections: [{ name: 'section_a', label: 'First' }],
|
||||
},
|
||||
{
|
||||
name: 'tab_b',
|
||||
label: 'New Tab',
|
||||
sections: [{ name: 'section_b', label: 'Second' }],
|
||||
},
|
||||
])
|
||||
|
||||
return () =>
|
||||
h('div', [
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
'data-testid': 'reload-tabs',
|
||||
onClick: () => {
|
||||
tabs.value = [
|
||||
{
|
||||
name: 'tab_a',
|
||||
label: 'New Tab',
|
||||
sections: [{ name: 'section_a_new', label: 'First' }],
|
||||
},
|
||||
{
|
||||
name: 'tab_b',
|
||||
label: 'New Tab',
|
||||
sections: [{ name: 'section_b_new', label: 'Second' }],
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
'Reload Tabs',
|
||||
),
|
||||
h(FieldLayout, {
|
||||
tabs: tabs.value,
|
||||
data: {},
|
||||
doctype: 'CRM Deal',
|
||||
isGridRow: true,
|
||||
tabIndex: fieldLayoutTabIndex.value,
|
||||
tabName: fieldLayoutTabName.value,
|
||||
'onUpdate:tabIndex': (value) => {
|
||||
fieldLayoutTabIndex.value = value
|
||||
},
|
||||
'onUpdate:tabName': (value) => {
|
||||
fieldLayoutTabName.value = value
|
||||
},
|
||||
}),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
const app = createApp(Parent)
|
||||
app.mount(root)
|
||||
|
||||
root.querySelectorAll('[role="tab"]')[1].click()
|
||||
await nextTick()
|
||||
expect(root.querySelector('[role="tabpanel"]').textContent).toBe('Second')
|
||||
|
||||
root.querySelector('[data-testid="reload-tabs"]').click()
|
||||
await nextTick()
|
||||
|
||||
expect(root.querySelector('[role="tabpanel"]').textContent).toBe('Second')
|
||||
|
||||
app.unmount()
|
||||
root.remove()
|
||||
})
|
||||
|
||||
it('restores the selected tab name from session storage after page remount', async () => {
|
||||
const { default: FieldLayout } = await import(
|
||||
'@/components/FieldLayout/FieldLayout.vue'
|
||||
)
|
||||
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
sessionStorage.clear()
|
||||
|
||||
const DataFieldsStub = defineComponent({
|
||||
setup() {
|
||||
const doctype = 'CRM Deal'
|
||||
const docname = 'DEAL-001'
|
||||
const fieldLayoutTabIndex = ref(0)
|
||||
const fieldLayoutTabName = ref('')
|
||||
const storageKey = `fieldLayoutTab:${doctype}:${docname}`
|
||||
const tabs = [
|
||||
{
|
||||
name: 'tab_a',
|
||||
label: 'New Tab',
|
||||
sections: [{ name: 'section_a', label: 'First' }],
|
||||
},
|
||||
{
|
||||
name: 'tab_b',
|
||||
label: 'New Tab',
|
||||
sections: [{ name: 'section_b', label: 'Second' }],
|
||||
},
|
||||
]
|
||||
|
||||
const storedFieldLayoutTabName = sessionStorage.getItem(storageKey)
|
||||
if (storedFieldLayoutTabName) {
|
||||
fieldLayoutTabName.value = storedFieldLayoutTabName
|
||||
}
|
||||
|
||||
return () =>
|
||||
h(FieldLayout, {
|
||||
tabs,
|
||||
data: {},
|
||||
doctype,
|
||||
isGridRow: true,
|
||||
tabIndex: fieldLayoutTabIndex.value,
|
||||
tabName: fieldLayoutTabName.value,
|
||||
'onUpdate:tabIndex': (value) => {
|
||||
fieldLayoutTabIndex.value = value
|
||||
},
|
||||
'onUpdate:tabName': (value) => {
|
||||
fieldLayoutTabName.value = value
|
||||
sessionStorage.setItem(storageKey, value)
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const Parent = defineComponent({
|
||||
setup() {
|
||||
const showPage = ref(true)
|
||||
|
||||
return () =>
|
||||
h('div', [
|
||||
h(
|
||||
'button',
|
||||
{
|
||||
'data-testid': 'remount-page',
|
||||
onClick: async () => {
|
||||
showPage.value = false
|
||||
await nextTick()
|
||||
showPage.value = true
|
||||
},
|
||||
},
|
||||
'Remount Page',
|
||||
),
|
||||
showPage.value && h(DataFieldsStub),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
const app = createApp(Parent)
|
||||
app.mount(root)
|
||||
|
||||
root.querySelectorAll('[role="tab"]')[1].click()
|
||||
await nextTick()
|
||||
expect(root.querySelector('[role="tabpanel"]').textContent).toBe('Second')
|
||||
|
||||
root.querySelector('[data-testid="remount-page"]').click()
|
||||
await nextTick()
|
||||
await nextTick()
|
||||
|
||||
expect(root.querySelector('[role="tabpanel"]').textContent).toBe('Second')
|
||||
|
||||
app.unmount()
|
||||
root.remove()
|
||||
sessionStorage.clear()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user