PluginProbe
Extendify / 3.1.0
Extendify v3.1.0
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / tests / playwright / QuickEdit / inline-text.spec.ts

inline-text.spec.ts in Extendify 3.1.0, at tests/playwright/QuickEdit/inline-text.spec.ts

272 lines 8.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { expect, test } from '../fixtures';
2
3 // Characterization: the primary inline-text journey for the rebuild's
4 // schema-driven editor. Each test pins one observable step from the
5 // user's perspective: admin pill → hover bar → BlockTextEditor mount →
6 // Save / Cmd+Enter / Esc / Cmd+Z. Headings drive the journey because
7 // the seed page leads with several h2s; the trailing paragraph pins
8 // that paragraphs now also resolve and get both pills (detectBlockType
9 // derives core/<X> from any wp-block-X class).
10 //
11 // The "Editor-role user → no admin-bar pill" coverage item
12 // is intentionally omitted: the EditModeLifecycleTest already pinned
13 // the gate at `current_user_can('edit_posts')`, which Editor users have.
14 // Replaying that gate in E2E would add no signal beyond what PHPUnit
15 // already characterizes.
16
17 const HEADING_1_ORIGINAL = 'First heading for inline-text testing.';
18 const HEADING_1_EDITED = 'First heading edited via Save click.';
19 const HEADING_2_ORIGINAL = 'Second heading for cancel testing.';
20 const HEADING_3_ORIGINAL = 'Third heading for cmd-enter testing.';
21 const HEADING_3_EDITED = 'Third heading edited via Cmd+Enter.';
22 const HEADING_4_ORIGINAL = 'Fourth heading for undo testing.';
23 const HEADING_4_EDITED = 'Fourth heading edited then undone.';
24 const PARAGRAPH_TEXT = 'Paragraphs only get Ask AI today, not Quick Edit.';
25
26 // Simple-toolbar replaces the WP admin bar on the live front end
27 // (Toolbar/Frontend.php hides `#wpadminbar`); its `#ext-tb-quick-edit`
28 // button is the user-facing Edit mode toggle.
29 const editModeToggle = (page) => page.locator('#ext-tb-quick-edit');
30
31 const hoverBar = (page) => page.locator('.extendify-quick-edit-bar');
32
33 const editorRichText = (page) =>
34 page
35 .locator('.extendify-quick-edit-canvas .block-editor-rich-text__editable')
36 .first();
37
38 const heading = (page, text: string) =>
39 page.locator('h2.wp-block-heading', { hasText: text });
40
41 const paragraph = (page, text: string) =>
42 page.locator('p.wp-block-paragraph', { hasText: text });
43
44 // Seeds edit-mode-on via the persist key so the hover bar's mouseover
45 // listener is wired by the time the spec hovers. Doing it through the
46 // pill works too but adds a click + an extra wait for the subscribe to
47 // propagate to the listener wiring in quick-edit.jsx.
48 const enableEditMode = async (page) => {
49 await page.addInitScript(() => {
50 window.localStorage.setItem(
51 'extendify-quick-edit-mode',
52 JSON.stringify({ state: { on: true }, version: 0 }),
53 );
54 });
55 };
56
57 // Edit mode now seeds its default from launch-completed, and this blueprint
58 // marks Launch completed — so the off-then-toggle-on test must set the off
59 // state explicitly rather than relying on the absence of persisted state.
60 const disableEditMode = async (page) => {
61 await page.addInitScript(() => {
62 window.localStorage.setItem(
63 'extendify-quick-edit-mode',
64 JSON.stringify({ state: { on: false }, version: 0 }),
65 );
66 });
67 };
68
69 const markPageForReloadDetection = async (page) => {
70 await page.evaluate(() => {
71 (window as unknown as { __qeNoReload: boolean }).__qeNoReload = true;
72 });
73 };
74
75 const pageDidNotReload = async (page) =>
76 page.evaluate(
77 () =>
78 (window as unknown as { __qeNoReload?: boolean }).__qeNoReload === true,
79 );
80
81 test.beforeEach(async ({ requestUtils }) => {
82 await requestUtils.login();
83 });
84
85 test('admin sees the Edit mode toggle and toggling it does not reload the page', async ({
86 page,
87 }) => {
88 await disableEditMode(page);
89 await page.goto('/');
90
91 const toggle = editModeToggle(page);
92 await expect(toggle).toBeVisible({ timeout: 15_000 });
93 await expect(toggle).toHaveAttribute('aria-checked', 'false');
94
95 await markPageForReloadDetection(page);
96 await toggle.click();
97
98 await expect(toggle).toHaveAttribute('aria-checked', 'true');
99 await expect(page.locator('html')).toHaveClass(/extendify-quick-edit-on/);
100 expect(await pageDidNotReload(page)).toBe(true);
101 });
102
103 test('hovering a heading in Edit mode shows the hover bar with Quick Edit + Ask AI pills', async ({
104 page,
105 }) => {
106 await enableEditMode(page);
107 await page.goto('/');
108
109 const target = heading(page, HEADING_1_ORIGINAL);
110 await expect(target).toBeVisible();
111 await target.hover();
112
113 const bar = hoverBar(page);
114 await expect(bar).toBeVisible();
115 await expect(bar.getByRole('button', { name: /Quick Edit/ })).toBeVisible();
116 await expect(bar.getByRole('button', { name: /Ask AI/ })).toBeVisible();
117 await expect(
118 page.locator('.extendify-quick-edit-hover-outline.is-visible'),
119 ).toBeVisible();
120 });
121
122 test('hovering a paragraph in Edit mode surfaces both Ask AI and Quick Edit pills', async ({
123 page,
124 }) => {
125 await enableEditMode(page);
126 await page.goto('/');
127
128 const para = paragraph(page, PARAGRAPH_TEXT);
129 await expect(para).toBeVisible();
130 await para.hover();
131
132 const bar = hoverBar(page);
133 await expect(bar).toBeVisible();
134 await expect(bar.getByRole('button', { name: /Ask AI/ })).toBeVisible();
135 await expect(bar.getByRole('button', { name: /Quick Edit/ })).toBeVisible();
136 });
137
138 test('clicking Quick Edit mounts the BlockTextEditor and focuses the rich-text input', async ({
139 page,
140 }) => {
141 await enableEditMode(page);
142 await page.goto('/');
143
144 await heading(page, HEADING_1_ORIGINAL).hover();
145 await hoverBar(page)
146 .getByRole('button', { name: /Quick Edit/ })
147 .click();
148
149 const editor = editorRichText(page);
150 await expect(editor).toBeVisible();
151 await expect(editor).toBeFocused();
152 });
153
154 test('Save persists the edit in-place and does not reload the page', async ({
155 page,
156 }) => {
157 await enableEditMode(page);
158 await page.goto('/');
159 await markPageForReloadDetection(page);
160
161 await heading(page, HEADING_1_ORIGINAL).hover();
162 await hoverBar(page)
163 .getByRole('button', { name: /Quick Edit/ })
164 .click();
165
166 const editor = editorRichText(page);
167 await expect(editor).toBeVisible();
168 await editor.press('ControlOrMeta+a');
169 await editor.pressSequentially(HEADING_1_EDITED);
170
171 const saved = page.waitForResponse(
172 (r) => r.url().includes('/quick-edit/save') && r.status() === 200,
173 );
174 await page.locator('[data-test="quick-edit-save"]').click();
175 await saved;
176
177 await expect(heading(page, HEADING_1_EDITED)).toBeVisible();
178 expect(await pageDidNotReload(page)).toBe(true);
179 });
180
181 test('Cmd+Enter persists the edit in-place and does not reload the page', async ({
182 page,
183 }) => {
184 await enableEditMode(page);
185 await page.goto('/');
186 await markPageForReloadDetection(page);
187
188 await heading(page, HEADING_3_ORIGINAL).hover();
189 await hoverBar(page)
190 .getByRole('button', { name: /Quick Edit/ })
191 .click();
192
193 const editor = editorRichText(page);
194 await expect(editor).toBeVisible();
195 await editor.press('ControlOrMeta+a');
196 await editor.pressSequentially(HEADING_3_EDITED);
197
198 const saved = page.waitForResponse(
199 (r) => r.url().includes('/quick-edit/save') && r.status() === 200,
200 );
201 await editor.press('ControlOrMeta+Enter');
202 await saved;
203
204 await expect(heading(page, HEADING_3_EDITED)).toBeVisible();
205 expect(await pageDidNotReload(page)).toBe(true);
206 });
207
208 test('Esc tears down the editor without saving', async ({ page }) => {
209 await enableEditMode(page);
210 await page.goto('/');
211
212 await heading(page, HEADING_2_ORIGINAL).hover();
213 await hoverBar(page)
214 .getByRole('button', { name: /Quick Edit/ })
215 .click();
216
217 const editor = editorRichText(page);
218 await expect(editor).toBeVisible();
219 await editor.press('ControlOrMeta+a');
220 await editor.pressSequentially('Throwaway text never committed');
221
222 let saveFired = false;
223 page.on('response', (r) => {
224 if (r.url().includes('/quick-edit/save')) saveFired = true;
225 });
226
227 await editor.press('Escape');
228
229 await expect(page.locator('.extendify-quick-edit-canvas')).not.toBeVisible();
230 await expect(heading(page, HEADING_2_ORIGINAL)).toBeVisible();
231 expect(saveFired).toBe(false);
232 });
233
234 test('Cmd+Z replays the prior save and the live DOM reverts after the reload', async ({
235 page,
236 }) => {
237 await enableEditMode(page);
238 await page.goto('/');
239
240 await heading(page, HEADING_4_ORIGINAL).hover();
241 await hoverBar(page)
242 .getByRole('button', { name: /Quick Edit/ })
243 .click();
244
245 const editor = editorRichText(page);
246 await expect(editor).toBeVisible();
247 await editor.press('ControlOrMeta+a');
248 await editor.pressSequentially(HEADING_4_EDITED);
249
250 const saved = page.waitForResponse(
251 (r) => r.url().includes('/quick-edit/save') && r.status() === 200,
252 );
253 await page.locator('[data-test="quick-edit-save"]').click();
254 await saved;
255
256 await expect(heading(page, HEADING_4_EDITED)).toBeVisible();
257
258 // undo replays the pre-edit body through /quick-edit/save and then
259 // reloads the page (state/undo.js' window.location.reload), so the
260 // assertion lives on the post-reload DOM.
261 const undoSaved = page.waitForResponse(
262 (r) => r.url().includes('/quick-edit/save') && r.status() === 200,
263 );
264 const reloaded = page.waitForLoadState('load');
265 await page.locator('body').press('ControlOrMeta+z');
266 await undoSaved;
267 await reloaded;
268
269 await expect(heading(page, HEADING_4_ORIGINAL)).toBeVisible();
270 await expect(heading(page, HEADING_4_EDITED)).toHaveCount(0);
271 });
272