fix(32): CR-04 take widget actions dispatched from connectedCallback

WidgetField assigned its element after append(), so an action the element
dispatched from connectedCallback (inside append) found no target and was
dropped without a result or an error state; widgets that load on connect
never started. The element is now the action target before it is connected.
Rebuilt the embedded admin dist.
This commit is contained in:
2026-10-27 15:25:24 +01:00
parent 5471b66b8f
commit d660cd05fa
6 changed files with 47 additions and 5 deletions

View File

@@ -158,8 +158,10 @@ onMounted(async () => {
created.setAttribute('label', props.field.actionLabel || props.field.label || props.field.name)
created.setAttribute('busy-label', t('backend::lang.extension.busy'))
created.addEventListener(WIDGET_EVENT, listener)
mountPoint.value.append(created)
// The element is the action target before it is connected: an action
// its connectedCallback dispatches (inside append) is taken, not dropped.
element = created
mountPoint.value.append(created)
status.value = 'ready'
} catch {
if (!unmounted) {

View File

@@ -49,6 +49,7 @@ interface Options {
assets?: () => Promise<string[]>
source?: typeof SOURCE | null
routes?: Record<string, Route>
attachTo?: HTMLElement
}
function mountWidget(options: Options = {}) {
@@ -59,6 +60,7 @@ function mountWidget(options: Options = {}) {
})
const calls = mockApi(options.routes ?? {})
const wrapper = mount(WidgetField, {
attachTo: options.attachTo,
props: {
field: options.field ?? lookupField(),
modelValue: undefined,
@@ -367,6 +369,44 @@ describe('summer-action (D-05, D-07)', () => {
})
})
describe('an action from connectedCallback (CR-04)', () => {
it('posts the action an element dispatches while it is being connected', async () => {
const tag = 'acme-demo-eager'
if (!customElements.get(tag)) {
customElements.define(
tag,
class extends HTMLElement {
connectedCallback(): void {
this.dispatchEvent(new CustomEvent(WIDGET_EVENT, { bubbles: true, composed: true, detail: { payload: { op: 'context' } } }))
}
},
)
}
const action = deferred()
const host = document.createElement('div')
document.body.append(host)
const { wrapper, calls } = mountWidget({
field: lookupField({ widget: tag }),
attachTo: host,
routes: { [`POST ${ACTION}`]: action.route },
})
await flushPromises()
const target = wrapper.find(tag).element as HTMLElement
expect(target.isConnected).toBe(true)
expect(target.hasAttribute('busy')).toBe(true)
const posts = requestsTo(calls, 'POST', ACTION)
expect(posts).toHaveLength(1)
const body = (await posts[0]!.json()) as Record<string, unknown>
expect(body.payload).toEqual({ op: 'context' })
const seen = results(target)
action.release({ body: { data: { message: '', fill: {} }, meta: {} } })
await flushPromises()
expect(target.hasAttribute('busy')).toBe(false)
expect(seen).toHaveLength(1)
wrapper.unmount()
})
})
describe('payload and data (summer-result)', () => {
const OK: Reply = { body: { data: { message: '', fill: {} }, meta: {} } }
const DATA_REPLY: Reply = {