| 1 |
import { expect, test } from '../../fixtures'; |
| 2 |
|
| 3 |
// Characterization: Cmd+Z after a wpforms field save. |
| 4 |
// WPFormsFieldModal stamps the inverse of its changes-bag into the undo |
| 5 |
// entry; performUndo's `wpformsReplay` branch POSTs those originals back |
| 6 |
// through /quick-edit/wpforms, which shallow-merges into the form's |
| 7 |
// serialized JSON. The forward and reverse paths use the exact same |
| 8 |
// endpoint shape — the controller doesn't distinguish "save" from "undo". |
| 9 |
|
| 10 |
const adminBarPill = (page) => page.locator('#ext-tb-quick-edit'); |
| 11 |
|
| 12 |
const hoverBar = (page) => page.locator('.extendify-quick-edit-bar'); |
| 13 |
|
| 14 |
const enableEditMode = async (page) => { |
| 15 |
await page.addInitScript(() => { |
| 16 |
window.localStorage.setItem( |
| 17 |
'extendify-quick-edit-mode', |
| 18 |
JSON.stringify({ state: { on: true }, version: 0 }), |
| 19 |
); |
| 20 |
}); |
| 21 |
}; |
| 22 |
|
| 23 |
const dialog = (page, name: RegExp) => page.getByRole('dialog', { name }); |
| 24 |
|
| 25 |
// Prefill modals mount their labeled inputs only after an async REST GET |
| 26 |
// resolves — until then they show a Spinner and keep Save disabled |
| 27 |
// (disabled={saving || !data}). On a starved CI runner that GET can outlast |
| 28 |
// expect()'s 5s default, racing the input assertions. Gate on Save enabled |
| 29 |
// (= data loaded) before touching the inputs. |
| 30 |
const waitForModalData = (modal) => |
| 31 |
expect(modal.getByRole('button', { name: /^Save$/ })).toBeEnabled({ |
| 32 |
timeout: 15_000, |
| 33 |
}); |
| 34 |
|
| 35 |
// The modal save reloads the page; QE must re-mount and re-bind its Cmd+Z |
| 36 |
// handler before the undo press. mount() appends #extendify-quick-edit-root |
| 37 |
// just before calling attachKeyboardUndo(), so that host is the post-reload |
| 38 |
// readiness signal — on a starved runner the bundle can still be loading when |
| 39 |
// the press would otherwise fire (root absent → no handler, undo no-ops). |
| 40 |
const waitForQuickEditReady = (page) => |
| 41 |
expect(page.locator('#extendify-quick-edit-root')).toBeAttached({ |
| 42 |
timeout: 15_000, |
| 43 |
}); |
| 44 |
|
| 45 |
const nameFieldContainer = (page) => |
| 46 |
page.locator('[data-extendify-quick-edit-wpform-field-id="1"]'); |
| 47 |
|
| 48 |
test.beforeEach(async ({ requestUtils }) => { |
| 49 |
await requestUtils.login(); |
| 50 |
}); |
| 51 |
|
| 52 |
test('Cmd+Z after a wpforms field save POSTs the original four props back through /quick-edit/wpforms and the rendered label + placeholder revert', async ({ |
| 53 |
page, |
| 54 |
}) => { |
| 55 |
await enableEditMode(page); |
| 56 |
await page.goto('/'); |
| 57 |
await expect(adminBarPill(page)).toBeVisible({ timeout: 15_000 }); |
| 58 |
|
| 59 |
const field = nameFieldContainer(page); |
| 60 |
await expect(field).toBeVisible({ timeout: 15_000 }); |
| 61 |
await field.scrollIntoViewIfNeeded(); |
| 62 |
// WPForms wraps fields in a `.wpforms-field-container` that the |
| 63 |
// Playwright actionability check sees as the topmost element when |
| 64 |
// hovering a child .wpforms-field. dispatchEvent fires the real DOM |
| 65 |
// mouseover event the hover-bar listens for on document and skips |
| 66 |
// the actionability check entirely. |
| 67 |
await field.dispatchEvent('mouseover'); |
| 68 |
await hoverBar(page) |
| 69 |
.getByRole('button', { name: /Quick Edit/ }) |
| 70 |
.click(); |
| 71 |
|
| 72 |
const modal = dialog(page, /Edit form field/i); |
| 73 |
await waitForModalData(modal); |
| 74 |
await modal.getByLabel(/^Label$/i).fill('Renamed via Quick Edit (undo)'); |
| 75 |
await modal.getByLabel(/^Placeholder$/i).fill('Renamed placeholder'); |
| 76 |
|
| 77 |
const saved = page.waitForResponse( |
| 78 |
(r) => |
| 79 |
r.url().includes('/quick-edit/wpforms') && |
| 80 |
r.request().method() === 'POST' && |
| 81 |
r.status() === 200, |
| 82 |
); |
| 83 |
const reloaded = page.waitForLoadState('load'); |
| 84 |
await modal.getByRole('button', { name: /^Save$/ }).click(); |
| 85 |
await saved; |
| 86 |
await reloaded; |
| 87 |
|
| 88 |
const renamedField = nameFieldContainer(page); |
| 89 |
await expect(renamedField).toBeVisible({ timeout: 15_000 }); |
| 90 |
await expect(renamedField).toContainText('Renamed via Quick Edit (undo)'); |
| 91 |
|
| 92 |
await waitForQuickEditReady(page); |
| 93 |
|
| 94 |
// WPFormsFieldModal stamps the inverse changes-bag — only the keys |
| 95 |
// touched on the forward save, paired with their pre-mutation values. |
| 96 |
const undoSaved = page.waitForResponse( |
| 97 |
(r) => |
| 98 |
r.url().includes('/quick-edit/wpforms') && |
| 99 |
r.request().method() === 'POST' && |
| 100 |
r.status() === 200, |
| 101 |
); |
| 102 |
const undoReloaded = page.waitForLoadState('load'); |
| 103 |
await page.locator('body').press('ControlOrMeta+z'); |
| 104 |
const undoRes = await undoSaved; |
| 105 |
const undoBody = JSON.parse(undoRes.request().postData() || '{}'); |
| 106 |
expect(undoBody.form_id).toBeGreaterThan(0); |
| 107 |
expect(undoBody.field_id).toBe(1); |
| 108 |
expect(undoBody.changes).toEqual({ |
| 109 |
label: 'Original Name Label', |
| 110 |
placeholder: 'Original placeholder', |
| 111 |
}); |
| 112 |
await undoReloaded; |
| 113 |
|
| 114 |
const restoredField = nameFieldContainer(page); |
| 115 |
await expect(restoredField).toBeVisible({ timeout: 15_000 }); |
| 116 |
await expect(restoredField).toContainText('Original Name Label'); |
| 117 |
await expect(restoredField.locator('input').first()).toHaveAttribute( |
| 118 |
'placeholder', |
| 119 |
'Original placeholder', |
| 120 |
); |
| 121 |
}); |
| 122 |
|